From c62e1437a24701029a412eb56e63c5c7d914b85a Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Sun, 12 Nov 2023 23:03:43 +0300 Subject: [PATCH 01/13] feat(errors): create classes for errors --- .../bridge/src/errors/MethodUnsupportedError.ts | 13 +++++++++++++ .../bridge/src/errors/ParameterUnsupportedError.ts | 13 +++++++++++++ packages/bridge/src/errors/index.ts | 2 ++ 3 files changed, 28 insertions(+) create mode 100644 packages/bridge/src/errors/MethodUnsupportedError.ts create mode 100644 packages/bridge/src/errors/ParameterUnsupportedError.ts create mode 100644 packages/bridge/src/errors/index.ts diff --git a/packages/bridge/src/errors/MethodUnsupportedError.ts b/packages/bridge/src/errors/MethodUnsupportedError.ts new file mode 100644 index 000000000..81dc55e96 --- /dev/null +++ b/packages/bridge/src/errors/MethodUnsupportedError.ts @@ -0,0 +1,13 @@ +import type { Version } from '@tma.js/utils'; + +import type { MethodName } from '../methods/index.js'; + +/** + * Error thrown in case, unsupported method was called. + */ +export class MethodUnsupportedError extends Error { + constructor(method: MethodName, version: Version) { + super(`Method "${method}" is unsupported in the Mini Apps version ${version}.`); + Object.setPrototypeOf(this, MethodUnsupportedError.prototype); + } +} diff --git a/packages/bridge/src/errors/ParameterUnsupportedError.ts b/packages/bridge/src/errors/ParameterUnsupportedError.ts new file mode 100644 index 000000000..08b53f0d4 --- /dev/null +++ b/packages/bridge/src/errors/ParameterUnsupportedError.ts @@ -0,0 +1,13 @@ +import type { Version } from '@tma.js/utils'; + +import type { MethodName } from '../methods/index.js'; + +/** + * Error thrown in case, unsupported parameter was used. + */ +export class ParameterUnsupportedError extends Error { + constructor(method: MethodName, param: string, version: Version) { + super(`Parameter "${param}" in method "${method}" is unsupported in the Mini Apps version ${version}.`); + Object.setPrototypeOf(this, ParameterUnsupportedError.prototype); + } +} diff --git a/packages/bridge/src/errors/index.ts b/packages/bridge/src/errors/index.ts new file mode 100644 index 000000000..d0224616f --- /dev/null +++ b/packages/bridge/src/errors/index.ts @@ -0,0 +1,2 @@ +export * from './MethodUnsupportedError.js'; +export * from './ParameterUnsupportedError.js'; From 26ba117bb0217f120b80b3abc9693ec238101364 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Sun, 12 Nov 2023 23:13:17 +0300 Subject: [PATCH 02/13] feat(methods,events): implement new web methods and events --- packages/bridge/src/events/emitter.ts | 1 + packages/bridge/src/events/events.ts | 6 ++ .../src/methods/{params.ts => methods.ts} | 13 +++- packages/bridge/tests/supports.ts | 71 ++++++++++++------- 4 files changed, 64 insertions(+), 27 deletions(-) rename packages/bridge/src/methods/{params.ts => methods.ts} (96%) diff --git a/packages/bridge/src/events/emitter.ts b/packages/bridge/src/events/emitter.ts index 9f6661db8..cceb3ed19 100644 --- a/packages/bridge/src/events/emitter.ts +++ b/packages/bridge/src/events/emitter.ts @@ -95,6 +95,7 @@ export function createEmitter(): EventEmitter { case 'back_button_pressed': case 'settings_button_pressed': case 'scan_qr_popup_closed': + case 'reload_iframe': return emit(eventType); // All other event listeners will receive unknown type of data. diff --git a/packages/bridge/src/events/events.ts b/packages/bridge/src/events/events.ts index dc194af0f..5256735ee 100644 --- a/packages/bridge/src/events/events.ts +++ b/packages/bridge/src/events/events.ts @@ -74,6 +74,12 @@ export interface Events { */ popup_closed: (payload: PopupClosedPayload) => void; + /** + * Parent iframe requested current iframe reload. + * @see https://docs.telegram-mini-apps.com/apps-communication/events#reload-iframe + */ + reload_iframe: () => void; + /** * The QR scanner scanned some QR and extracted its content. * @param payload - event payload. diff --git a/packages/bridge/src/methods/params.ts b/packages/bridge/src/methods/methods.ts similarity index 96% rename from packages/bridge/src/methods/params.ts rename to packages/bridge/src/methods/methods.ts index f34494acb..a31edfb2a 100644 --- a/packages/bridge/src/methods/params.ts +++ b/packages/bridge/src/methods/methods.ts @@ -31,7 +31,18 @@ export interface MethodsParams { * version of Telegram. As a result, Mini App will receive `set_custom_style` event. * @see https://docs.telegram-mini-apps.com/apps-communication/methods#iframe-ready */ - iframe_ready: CreateParams; + iframe_ready: CreateParams<{ + /** + * True, if current Mini App supports native reloading. + */ + reload_supported?: boolean; + } | undefined>; + + /** + * Notifies parent iframe about the current iframe is going to reload. + * @see https://docs.telegram-mini-apps.com/apps-communication/methods#iframe-will-reload + */ + iframe_will_reload: CreateParams; /** * Closes Mini App. diff --git a/packages/bridge/tests/supports.ts b/packages/bridge/tests/supports.ts index a8fb13e4e..08a837958 100644 --- a/packages/bridge/tests/supports.ts +++ b/packages/bridge/tests/supports.ts @@ -35,32 +35,51 @@ function increaseVersion(version: Version, amount: number): string { return `${version.slice(0, lastDotIndex + 1)}${lastPart + amount}`; } -describe('supports.ts', () => { - describe('supports', () => { - const tests: Test[] = [ - ['any', [ - 'iframe_ready', 'web_app_close', 'web_app_data_send', 'web_app_expand', - 'web_app_open_link', 'web_app_ready', 'web_app_request_theme', - 'web_app_request_viewport', 'web_app_setup_main_button', - 'web_app_setup_closing_behavior', - ]], - ['6.1', [ - 'web_app_open_tg_link', 'web_app_open_invoice', 'web_app_setup_back_button', - 'web_app_set_background_color', 'web_app_set_header_color', - 'web_app_trigger_haptic_feedback', - ]], - ['6.2', ['web_app_open_popup']], - ['6.4', [ - 'web_app_read_text_from_clipboard', 'web_app_close_scan_qr_popup', - 'web_app_close_scan_qr_popup', ['web_app_open_link', 'try_instant_view'], - ]], - ['6.7', ['web_app_switch_inline_query']], - ['6.9', [ - 'web_app_invoke_custom_method', 'web_app_request_write_access', 'web_app_request_phone', - ['web_app_set_header_color', 'color'], - ]], - ['6.10', ['web_app_setup_settings_button']], - ]; +describe('supports', () => { + const tests: Test[] = [ + ['any', [ + 'iframe_ready', + 'iframe_will_reload', + 'web_app_close', + 'web_app_data_send', + 'web_app_expand', + 'web_app_open_link', + 'web_app_ready', + 'web_app_request_theme', + 'web_app_request_viewport', + 'web_app_setup_main_button', + 'web_app_setup_closing_behavior', + ]], + ['6.1', [ + 'web_app_open_tg_link', + 'web_app_open_invoice', + 'web_app_setup_back_button', + 'web_app_set_background_color', + 'web_app_set_header_color', + 'web_app_trigger_haptic_feedback', + ]], + ['6.2', [ + 'web_app_open_popup', + ]], + ['6.4', [ + 'web_app_read_text_from_clipboard', + 'web_app_close_scan_qr_popup', + 'web_app_close_scan_qr_popup', + ['web_app_open_link', 'try_instant_view'], + ]], + ['6.7', [ + 'web_app_switch_inline_query', + ]], + ['6.9', [ + 'web_app_invoke_custom_method', + 'web_app_request_write_access', + 'web_app_request_phone', + ['web_app_set_header_color', 'color'], + ]], + ['6.10', [ + 'web_app_setup_settings_button', + ]], + ]; tests.forEach(([version, methods]) => { if (version === 'any') { From b5a481847b190007823e92a54a02c08857c634fa Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Sun, 12 Nov 2023 23:13:41 +0300 Subject: [PATCH 03/13] feat(functions): implement createPostEvent function --- .../bridge/src/methods/createPostEvent.ts | 39 +++++++++++++++++++ .../bridge/tests/methods/createPostEvent.ts | 39 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 packages/bridge/src/methods/createPostEvent.ts create mode 100644 packages/bridge/tests/methods/createPostEvent.ts diff --git a/packages/bridge/src/methods/createPostEvent.ts b/packages/bridge/src/methods/createPostEvent.ts new file mode 100644 index 000000000..4aaae19eb --- /dev/null +++ b/packages/bridge/src/methods/createPostEvent.ts @@ -0,0 +1,39 @@ +import { isRecord, type Version } from '@tma.js/utils'; + +import { supports } from '../supports.js'; +import { MethodUnsupportedError, ParameterUnsupportedError } from '../errors/index.js'; +import { postEvent, type PostEvent } from './index.js'; + +/** + * Creates function which checks if specified method and parameters are supported. In case, + * method or parameters are unsupported, an error will be thrown. + * @param version - Telegram Mini Apps version. + * @throws {MethodUnsupportedError} Method is unsupported. + * @throws {ParameterUnsupportedError} Method parameter is unsupported. + */ +export function createPostEvent(version: Version): PostEvent { + return (method: any, params: any) => { + // Firstly, check if method itself is supported. + if (!supports(method, version)) { + throw new MethodUnsupportedError(method, version); + } + + // Method could use parameters, which are supported only in specific versions of Telegram + // Mini Apps. + if (isRecord(params)) { + let validateParam: string | undefined; + + if (method === 'web_app_open_link' && 'try_instant_view' in params) { + validateParam = 'try_instant_view'; + } else if (method === 'web_app_set_header_color' && 'color' in params) { + validateParam = 'color'; + } + + if (validateParam && !supports(method, validateParam, version)) { + throw new ParameterUnsupportedError(method, validateParam, version); + } + } + + return postEvent(method, params); + }; +} diff --git a/packages/bridge/tests/methods/createPostEvent.ts b/packages/bridge/tests/methods/createPostEvent.ts new file mode 100644 index 000000000..40f9790d0 --- /dev/null +++ b/packages/bridge/tests/methods/createPostEvent.ts @@ -0,0 +1,39 @@ +import { describe, expect, vi, it } from 'vitest'; + +import * as postEventModule from '../../src/methods/postEvent.js'; +import { createPostEvent } from '../../src/index.js'; + +vi.mock('../../src/methods/postEvent.js', () => ({ + postEvent: vi.fn(), +})); + +describe('createPostEvent', () => { + it('should throw error if passed method is unsupported in specified version', () => { + const postEvent = createPostEvent('6.0'); + expect(() => postEvent('web_app_request_write_access')) + .toThrow('Method "web_app_request_write_access" is unsupported in the Mini Apps version 6.0.'); + }); + + it('should throw error if passed method parameter is unsupported in specified version', () => { + const postEvent = createPostEvent('6.3'); + expect(() => postEvent('web_app_open_link', { + url: '', + try_instant_view: true, + })) + .toThrow('Parameter "try_instant_view" in method "web_app_open_link" is unsupported in the Mini Apps version 6.3.'); + + expect(() => postEvent('web_app_set_header_color', { + color: '#aaaaaa', + })) + .toThrow('Parameter "color" in method "web_app_set_header_color" is unsupported in the Mini Apps version 6.3.'); + }); + + it('should call global postEvent function', () => { + const postEvent = createPostEvent('6.3'); + const spy = vi.spyOn(postEventModule, 'postEvent'); + + postEvent('web_app_request_viewport'); + expect(spy).toHaveBeenCalledOnce(); + expect(spy).toHaveBeenCalledWith('web_app_request_viewport', undefined); + }); +}); From d74f7de089ef45ee23bb71c3141fede03706f29c Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Sun, 12 Nov 2023 23:17:34 +0300 Subject: [PATCH 04/13] feat(theme-params): add new theme params keys --- packages/bridge/src/events/parsing.ts | 12 +++++++++--- packages/bridge/src/events/payloads.ts | 23 ++++++++++++++++------- packages/bridge/tests/events/parsing.ts | 21 ++++++++++----------- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/packages/bridge/src/events/parsing.ts b/packages/bridge/src/events/parsing.ts index f3c36fba5..3cef6f9d6 100644 --- a/packages/bridge/src/events/parsing.ts +++ b/packages/bridge/src/events/parsing.ts @@ -33,13 +33,19 @@ const windowWidthParser = createValueParserGenerator( */ export const themeChangedPayload = json({ theme_params: json({ + accent_text_color: rgbOptional, bg_color: rgbOptional, - text_color: rgbOptional, - hint_color: rgbOptional, - link_color: rgbOptional, button_color: rgbOptional, button_text_color: rgbOptional, + destructive_text_color: rgbOptional, + header_bg_color: rgbOptional, + hint_color: rgbOptional, + link_color: rgbOptional, secondary_bg_color: rgbOptional, + section_bg_color: rgbOptional, + section_header_text_color: rgbOptional, + subtitle_text_color: rgbOptional, + text_color: rgbOptional, }), }); diff --git a/packages/bridge/src/events/payloads.ts b/packages/bridge/src/events/payloads.ts index 7963d75c9..6765a62ae 100644 --- a/packages/bridge/src/events/payloads.ts +++ b/packages/bridge/src/events/payloads.ts @@ -54,19 +54,28 @@ export interface QrTextReceivedPayload { data?: string; } +export type ThemeParamsKey = + | 'accent_text_color' + | 'bg_color' + | 'button_color' + | 'button_text_color' + | 'destructive_text_color' + | 'header_bg_color' + | 'hint_color' + | 'link_color' + | 'secondary_bg_color' + | 'section_header_text_color' + | 'section_bg_color' + | 'subtitle_text_color' + | 'text_color'; + export interface ThemeChangedPayload { /** * Map where the key is a theme stylesheet key and value is the corresponding color in * `#RRGGBB` format. */ theme_params: { - bg_color?: RGB; - text_color?: RGB; - hint_color?: RGB; - link_color?: RGB; - button_color?: RGB; - button_text_color?: RGB; - secondary_bg_color?: RGB; + [Key in ThemeParamsKey]?: RGB; }; } diff --git a/packages/bridge/tests/events/parsing.ts b/packages/bridge/tests/events/parsing.ts index f75a4d79b..131271a77 100644 --- a/packages/bridge/tests/events/parsing.ts +++ b/packages/bridge/tests/events/parsing.ts @@ -15,24 +15,23 @@ describe('events', () => { const values = [ { theme_params: { + accent_text_color: '#aaccbb', bg_color: '#ffaabb', - text_color: '#bbaadd', - hint_color: '#113322', - link_color: '#882133', button_color: '#faaafa', button_text_color: '#666271', + destructive_text_color: '#111332', + header_bg_color: '#aab133', + hint_color: '#113322', + link_color: '#882133', secondary_bg_color: '#2231aa', + section_bg_color: '#111332', + section_header_text_color: '#111332', + subtitle_text_color: '#111332', + text_color: '#bbaadd', }, }, { - theme_params: { - bg_color: '#ffaabb', - text_color: '#bbaadd', - hint_color: '#113322', - link_color: '#882133', - button_color: '#faaafa', - button_text_color: '#666271', - }, + theme_params: {}, }, ]; From dc709a078dfc5c80013be46b0d6d312c2ee80d88 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Sun, 12 Nov 2023 23:34:52 +0300 Subject: [PATCH 05/13] refactor(imports,type names): refactor imports, type names and descriptions --- packages/bridge/src/events/events.ts | 9 ++-- packages/bridge/src/index.ts | 1 + packages/bridge/src/methods/index.ts | 9 ++-- packages/bridge/src/methods/methods.ts | 32 +++++++------ packages/bridge/src/methods/postEvent.ts | 2 +- packages/bridge/src/request.ts | 57 +++++++++++------------- 6 files changed, 56 insertions(+), 54 deletions(-) diff --git a/packages/bridge/src/events/events.ts b/packages/bridge/src/events/events.ts index 5256735ee..98c504879 100644 --- a/packages/bridge/src/events/events.ts +++ b/packages/bridge/src/events/events.ts @@ -1,8 +1,8 @@ import type { EventEmitter as UtilEventEmitter, - EventName as UtilEventName, EventListener as UtilEventListener, - EventParams as UtilEventParams, AnySubscribeListener, + EventParams as UtilEventParams, + AnySubscribeListener, } from '@tma.js/event-emitter'; import type { IsNever, Not } from '@tma.js/util-types'; @@ -139,7 +139,7 @@ export interface Events { /** * Any known event name. */ -export type EventName = UtilEventName; +export type EventName = keyof Events; /** * Parameters of specified event. @@ -149,8 +149,7 @@ export type EventParams = UtilEventParams[0]; /** * Returns event listener for specified event name. */ -export type EventListener = - UtilEventListener; +export type EventListener = UtilEventListener; /** * Event emitter, based describe events map. diff --git a/packages/bridge/src/index.ts b/packages/bridge/src/index.ts index bb5e55775..6d8b359de 100644 --- a/packages/bridge/src/index.ts +++ b/packages/bridge/src/index.ts @@ -1,3 +1,4 @@ +export * from './errors/index.js'; export * from './events/index.js'; export * from './methods/index.js'; export * from './env.js'; diff --git a/packages/bridge/src/methods/index.ts b/packages/bridge/src/methods/index.ts index 2840186fd..f60300ca3 100644 --- a/packages/bridge/src/methods/index.ts +++ b/packages/bridge/src/methods/index.ts @@ -1,5 +1,6 @@ -export type * from './haptic.js'; -export type * from './invoke-custom-method.js'; -export type * from './params.js'; -export type * from './popup.js'; +export * from './createPostEvent.js'; +export * from './haptic.js'; +export * from './invoke-custom-method.js'; +export * from './methods.js'; +export * from './popup.js'; export * from './postEvent.js'; diff --git a/packages/bridge/src/methods/methods.ts b/packages/bridge/src/methods/methods.ts index a31edfb2a..2b91a6fb3 100644 --- a/packages/bridge/src/methods/methods.ts +++ b/packages/bridge/src/methods/methods.ts @@ -16,16 +16,16 @@ export type HeaderColorKey = 'bg_color' | 'secondary_bg_color'; */ export type SwitchInlineQueryChatType = 'users' | 'bots' | 'groups' | 'channels'; -type CreateParams

= never> = { - params: P; +interface CreateParams = never> { + params: Params; supportCheckKey: SupportCheckKey; -}; +} /** * Describes list of events and their parameters that could be posted by Bridge. * @see https://docs.telegram-mini-apps.com/apps-communication/methods */ -export interface MethodsParams { +export interface Methods { /** * Notifies parent iframe about the current frame is ready. This method is only used in the Web * version of Telegram. As a result, Mini App will receive `set_custom_style` event. @@ -330,40 +330,44 @@ export interface MethodsParams { } /** - * Any post-available event name. + * Any Telegram Mini Apps known method name. */ -export type MethodName = keyof MethodsParams; +export type MethodName = keyof Methods; /** * Returns parameters for specified post-available event. */ -export type MethodParams = MethodsParams[E]['params']; +export type MethodParams = Methods[M]['params']; /** - * Returns true in case, method has parameters. + * True if specified method accepts parameters. */ -export type MethodHasParams = Not>>; +export type MethodAcceptParams = + Not, undefined>>>; /** * Any post-available event name which does not require arguments. */ export type EmptyMethodName = { - [E in MethodName]: IsNever> extends true ? E : never; + [M in MethodName]: undefined extends MethodParams ? M : never; }[MethodName]; /** * Any post-available event name which require arguments. */ -export type NonEmptyMethodName = Exclude; +export type NonEmptyMethodName = { + [M in MethodName]: MethodAcceptParams extends true ? M : never; +}[MethodName]; /** * Method names which could be used in supportsParam method. */ -export type HasCheckSupportMethodName = { - [E in MethodName]: IsNever extends true ? never : E; +export type HasCheckSupportKeyMethod = { + [M in MethodName]: IsNever extends true ? never : M; }[MethodName]; /** * Method parameter which can be checked via support method. */ -export type HasCheckSupportMethodParam = MethodsParams[M]['supportCheckKey']; +export type HasCheckSupportMethodParam = + Methods[M]['supportCheckKey']; diff --git a/packages/bridge/src/methods/postEvent.ts b/packages/bridge/src/methods/postEvent.ts index 8080594ef..5d09c2252 100644 --- a/packages/bridge/src/methods/postEvent.ts +++ b/packages/bridge/src/methods/postEvent.ts @@ -9,7 +9,7 @@ import type { MethodName, MethodParams, NonEmptyMethodName, -} from './params.js'; +} from './methods.js'; interface PostEventOptions { /** diff --git a/packages/bridge/src/request.ts b/packages/bridge/src/request.ts index 2b0d71cbd..c5602553e 100644 --- a/packages/bridge/src/request.ts +++ b/packages/bridge/src/request.ts @@ -1,24 +1,24 @@ import { withTimeout, isRecord } from '@tma.js/utils'; - import type { And, If, IsNever } from '@tma.js/util-types'; import { postEvent as defaultPostEvent, type PostEvent } from './methods/postEvent.js'; import { on, type EventName, type EventParams, type EventHasParams } from './events/index.js'; import type { - EmptyMethodName, MethodHasParams, + EmptyMethodName, + MethodAcceptParams, MethodName, MethodParams, NonEmptyMethodName, -} from './methods/params.js'; +} from './methods/methods.js'; /** * Names of methods, which require passing "req_id" parameter. */ -type MethodNameWithRequestId = { - [Method in MethodName]: If< - And, MethodParams extends { req_id: string } ? true : false>, - Method, +type MethodWithRequestId = { + [M in MethodName]: If< + And, MethodParams extends { req_id: string } ? true : false>, + M, never >; }[MethodName]; @@ -26,10 +26,10 @@ type MethodNameWithRequestId = { /** * Names of events, which contain "req_id" parameter. */ -type EventNameWithRequestId = { - [Event in EventName]: If< - And, EventParams extends { req_id: string } ? true : false>, - Event, +type EventWithRequestId = { + [E in EventName]: If< + And, EventParams extends { req_id: string } ? true : false>, + E, never >; }[EventName]; @@ -64,15 +64,12 @@ export interface RequestOptionsAdvanced extends RequestOptions { * @param event - event or events to listen. * @param options - additional execution options. */ -export function request< - Method extends MethodNameWithRequestId, - Event extends EventNameWithRequestId, ->( - method: Method, - params: MethodParams, - event: Event | Event[], +export function request( + method: M, + params: MethodParams, + event: E | E[], options?: RequestOptions, -): Promise>; +): Promise>; /** * Calls specified TWA method and captures one of the specified events. Returns promise @@ -81,11 +78,11 @@ export function request< * @param event - event or events to listen. * @param options - additional execution options. */ -export function request( - method: Method, - event: Event | Event[], - options?: RequestOptionsAdvanced>, -): Promise>; +export function request( + method: M, + event: E | E[], + options?: RequestOptionsAdvanced>, +): Promise>; /** * Calls specified TWA method and captures one of the specified events. Returns promise @@ -95,12 +92,12 @@ export function request * @param event - event or events to listen * @param options - additional execution options. */ -export function request( - method: Method, - params: MethodParams, - event: Event | Event[], - options?: RequestOptionsAdvanced>, -): Promise>; +export function request( + method: M, + params: MethodParams, + event: E | E[], + options?: RequestOptionsAdvanced>, +): Promise>; export function request( method: MethodName, From 2cbb5192abcccff1e57701636f3df5568823f802 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Sun, 12 Nov 2023 23:35:23 +0300 Subject: [PATCH 06/13] fix(functions): remove detectSupportParams utility --- packages/bridge/src/supports.ts | 55 ++++++++++----------------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/packages/bridge/src/supports.ts b/packages/bridge/src/supports.ts index 4d665fca2..01aa3fc95 100644 --- a/packages/bridge/src/supports.ts +++ b/packages/bridge/src/supports.ts @@ -2,39 +2,17 @@ import { compareVersions, type Version } from '@tma.js/utils'; import type { HasCheckSupportMethodParam, - HasCheckSupportMethodName, + HasCheckSupportKeyMethod, MethodName, - MethodParams, - NonEmptyMethodName, } from './methods/index.js'; -function lessOrEqual(a: Version, b: Version): boolean { - return compareVersions(a, b) <= 0; -} - /** - * By specified method and parameters extracts properties which could be used by - * supports function as TWA method parameter. - * @param method - TWA method. - * @param params - method parameters. + * Returns true if "a" version is less than or equal to "b" version. + * @param a + * @param b */ -export function detectSupportParams( - method: M, - params: MethodParams, -): HasCheckSupportMethodParam[] { - if (method === 'web_app_open_link') { - if ('try_instant_view' in params) { - return ['try_instant_view']; - } - } - - if (method === 'web_app_set_header_color') { - if ('color' in params) { - return ['color']; - } - } - - return []; +function versionLessOrEqual(a: Version, b: Version): boolean { + return compareVersions(a, b) <= 0; } /** @@ -43,17 +21,19 @@ export function detectSupportParams( * @param param - method parameter * @param inVersion - platform version. */ -export function supports( +export function supports( method: M, param: HasCheckSupportMethodParam, inVersion: Version, ): boolean; + /** * Returns true in case, specified method is supported in passed version. * @param method - method name. * @param inVersion - platform version. */ export function supports(method: MethodName, inVersion: Version): boolean; + export function supports( method: MethodName, paramOrVersion: Version | string, @@ -63,18 +43,17 @@ export function supports( if (typeof inVersion === 'string') { if (method === 'web_app_open_link') { if (paramOrVersion === 'try_instant_view') { - return lessOrEqual('6.4', inVersion); + return versionLessOrEqual('6.4', inVersion); } } if (method === 'web_app_set_header_color') { if (paramOrVersion === 'color') { - return lessOrEqual('6.9', inVersion); + return versionLessOrEqual('6.9', inVersion); } } } - // Method name, target version. switch (method) { case 'web_app_open_tg_link': case 'web_app_open_invoice': @@ -82,21 +61,21 @@ export function supports( case 'web_app_set_background_color': case 'web_app_set_header_color': case 'web_app_trigger_haptic_feedback': - return lessOrEqual('6.1', paramOrVersion); + return versionLessOrEqual('6.1', paramOrVersion); case 'web_app_open_popup': - return lessOrEqual('6.2', paramOrVersion); + return versionLessOrEqual('6.2', paramOrVersion); case 'web_app_close_scan_qr_popup': case 'web_app_open_scan_qr_popup': case 'web_app_read_text_from_clipboard': - return lessOrEqual('6.4', paramOrVersion); + return versionLessOrEqual('6.4', paramOrVersion); case 'web_app_switch_inline_query': - return lessOrEqual('6.7', paramOrVersion); + return versionLessOrEqual('6.7', paramOrVersion); case 'web_app_invoke_custom_method': case 'web_app_request_write_access': case 'web_app_request_phone': - return lessOrEqual('6.9', paramOrVersion); + return versionLessOrEqual('6.9', paramOrVersion); case 'web_app_setup_settings_button': - return lessOrEqual('6.10', paramOrVersion); + return versionLessOrEqual('6.10', paramOrVersion); default: return true; } From 664eae48264356e20b3716b6de303844ddb6a281 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Sun, 12 Nov 2023 23:54:43 +0300 Subject: [PATCH 07/13] tests(all): refactor all tests and add missing --- packages/bridge/tests/env.ts | 83 +++-- packages/bridge/tests/events/emitter.ts | 230 +++++++------- packages/bridge/tests/events/off.ts | 4 +- packages/bridge/tests/events/on.ts | 4 +- .../bridge/tests/events/onTelegramEvent.ts | 4 +- packages/bridge/tests/events/once.ts | 4 +- packages/bridge/tests/events/subscribe.ts | 4 +- packages/bridge/tests/events/unsubscribe.ts | 4 +- packages/bridge/tests/globals.ts | 36 +-- packages/bridge/tests/request.ts | 298 +++++++++--------- packages/bridge/tests/supports.ts | 122 +++---- 11 files changed, 381 insertions(+), 412 deletions(-) diff --git a/packages/bridge/tests/env.ts b/packages/bridge/tests/env.ts index 5a487efcc..e15013da7 100644 --- a/packages/bridge/tests/env.ts +++ b/packages/bridge/tests/env.ts @@ -1,54 +1,53 @@ import { expect, it, vi, afterEach, afterAll, describe } from 'vitest'; + import { hasExternalNotify, hasWebviewProxy, isIframe } from '../src/env.js'; const emptyFunction = () => { }; -describe('env.ts', () => { - describe('hasExternalNotify', () => { - it('should return true if passed object contains path property "external.notify" and "notify" is a function property.', () => { - expect(hasExternalNotify({})).toBe(false); - expect(hasExternalNotify({ external: {} })).toBe(false); - expect(hasExternalNotify({ external: { notify: [] } })).toBe(false); - expect(hasExternalNotify({ external: { notify: emptyFunction } })).toBe(true); - }); +describe('hasExternalNotify', () => { + it('should return true if passed object contains path property "external.notify" and "notify" is a function property.', () => { + expect(hasExternalNotify({})).toBe(false); + expect(hasExternalNotify({ external: {} })).toBe(false); + expect(hasExternalNotify({ external: { notify: [] } })).toBe(false); + expect(hasExternalNotify({ external: { notify: emptyFunction } })).toBe(true); + }); +}); + +describe('hasWebviewProxy', () => { + it('should return true if passed object contains path property "TelegramWebviewProxy.postEvent" and "postEvent" is a function property.', () => { + expect(hasWebviewProxy({})).toBe(false); + expect(hasWebviewProxy({ TelegramWebviewProxy: {} })).toBe(false); + expect(hasWebviewProxy({ TelegramWebviewProxy: { postEvent: [] } })).toBe(false); + expect(hasWebviewProxy({ TelegramWebviewProxy: { postEvent: emptyFunction } })).toBe(true); }); +}); + +describe('isIframe', () => { + const windowSpy = vi.spyOn(window, 'window', 'get'); + + afterEach(() => { + windowSpy.mockReset(); + }); + + afterAll(() => { + windowSpy.mockRestore(); + }); + + it('should return true in case window.self !== window.top. Otherwise, false.', () => { + windowSpy.mockImplementation(() => ({ self: 900, top: 1000 }) as any); + expect(isIframe()).toBe(true); - describe('hasWebviewProxy', () => { - it('should return true if passed object contains path property "TelegramWebviewProxy.postEvent" and "postEvent" is a function property.', () => { - expect(hasWebviewProxy({})).toBe(false); - expect(hasWebviewProxy({ TelegramWebviewProxy: {} })).toBe(false); - expect(hasWebviewProxy({ TelegramWebviewProxy: { postEvent: [] } })).toBe(false); - expect(hasWebviewProxy({ TelegramWebviewProxy: { postEvent: emptyFunction } })).toBe(true); - }); + windowSpy.mockImplementation(() => ({ self: 900, top: 900 }) as any); + expect(isIframe()).toBe(false); }); - describe('isIframe', () => { - const windowSpy = vi.spyOn(window, 'window', 'get'); - - afterEach(() => { - windowSpy.mockReset(); - }); - - afterAll(() => { - windowSpy.mockRestore(); - }); - - it('should return true in case window.self !== window.top. Otherwise, false.', () => { - windowSpy.mockImplementation(() => ({ self: 900, top: 1000 }) as any); - expect(isIframe()).toBe(true); - - windowSpy.mockImplementation(() => ({ self: 900, top: 900 }) as any); - expect(isIframe()).toBe(false); - }); - - it('should return true in case window.self getter threw an error', () => { - windowSpy.mockImplementation(() => ({ - get self() { - throw new Error(); - }, - }) as any); - expect(isIframe()).toBe(true); - }); + it('should return true in case window.self getter threw an error', () => { + windowSpy.mockImplementation(() => ({ + get self() { + throw new Error(); + }, + }) as any); + expect(isIframe()).toBe(true); }); }); diff --git a/packages/bridge/tests/events/emitter.ts b/packages/bridge/tests/events/emitter.ts index 67c60076a..b0fd8151d 100644 --- a/packages/bridge/tests/events/emitter.ts +++ b/packages/bridge/tests/events/emitter.ts @@ -1,8 +1,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { createEmitter, singletonEmitter } from '../../src/events/emitter.js'; -import { createWindow, type WindowSpy } from '../../__test-utils__/createWindow.js'; -import { dispatchWindowMessageEvent } from '../../__test-utils__/dispatchWindowMessageEvent.js'; +import { createWindow, type WindowSpy } from '../../test-utils/createWindow.js'; +import { dispatchWindowMessageEvent } from '../../test-utils/dispatchWindowMessageEvent.js'; import type { EventName, EventParams } from '../../src/index.js'; @@ -11,9 +11,9 @@ type TestCase = | EventParams; type TestCases = { - [Event in EventName]: EventParams extends undefined - ? [Event] - : [Event, TestCase | TestCase[]]; + [E in EventName]: EventParams extends undefined + ? [E] + : [E, TestCase | TestCase[]]; }[EventName][]; let windowSpy: WindowSpy; @@ -29,128 +29,124 @@ afterEach(() => { windowSpy.mockRestore(); }); -describe('events', () => { - describe('emitter.ts', () => { - describe('createEmitter', () => { - it('should emit "viewport_changed" event in case, window changed its size', () => { - const emitter = createEmitter(); - const spy = vi.fn(); +describe('createEmitter', () => { + it('should emit "viewport_changed" event in case, window changed its size', () => { + const emitter = createEmitter(); + const spy = vi.fn(); - emitter.on('viewport_changed', spy); + emitter.on('viewport_changed', spy); - window.dispatchEvent(new CustomEvent('resize')); + window.dispatchEvent(new CustomEvent('resize')); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith({ - width: 1920, - height: 1080, - is_state_stable: true, - is_expanded: true, - }); - }); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith({ + width: 1920, + height: 1080, + is_state_stable: true, + is_expanded: true, + }); + }); + + describe('events handling', () => { + const testCases: TestCases = [ + ['viewport_changed', { + height: 120, + width: 300, + is_expanded: true, + is_state_stable: false, + }], + ['theme_changed', { + theme_params: { + bg_color: '#aabbdd', + text_color: '#113322', + hint_color: '#132245', + link_color: '#133322', + button_color: '#a23135', + button_text_color: '#aa213f', + }, + }], + ['popup_closed', [ + [{ button_id: 'ok' }, { button_id: 'ok' }], + [{ button_id: null }, {}], + [{ button_id: undefined }, {}], + [null, {}], + [undefined, {}], + ]], + ['set_custom_style', '.scroll {}'], + ['qr_text_received', { data: 'some QR data' }], + ['main_button_pressed'], + ['back_button_pressed'], + ['settings_button_pressed'], + ['scan_qr_popup_closed'], + ['clipboard_text_received', { + req_id: 'request id', + data: 'clipboard value', + }], + ['invoice_closed', { slug: '&&*Sh1j213kx', status: 'PAID' }], + ['phone_requested', { status: 'sent' }], + ['custom_method_invoked', [ + [{ req_id: '1', result: 'My result' }], + [{ req_id: '2', error: 'Something is wrong' }], + ]], + ['write_access_requested', { status: 'allowed' }], + ['unknown_event', [ + ['hello', 'hello'], + [{ there: true }, { there: true }], + ]] as any, + ]; + + testCases.forEach(([event, inputOrCaseOrCases]) => { + it(`should correctly handle "${event}" event data`, () => { + const spy = vi.fn(); + const emitter = createEmitter(); - describe('events handling', () => { - const testCases: TestCases = [ - ['viewport_changed', { - height: 120, - width: 300, - is_expanded: true, - is_state_stable: false, - }], - ['theme_changed', { - theme_params: { - bg_color: '#aabbdd', - text_color: '#113322', - hint_color: '#132245', - link_color: '#133322', - button_color: '#a23135', - button_text_color: '#aa213f', - }, - }], - ['popup_closed', [ - [{ button_id: 'ok' }, { button_id: 'ok' }], - [{ button_id: null }, {}], - [{ button_id: undefined }, {}], - [null, {}], - [undefined, {}], - ]], - ['set_custom_style', '.scroll {}'], - ['qr_text_received', { data: 'some QR data' }], - ['main_button_pressed'], - ['back_button_pressed'], - ['settings_button_pressed'], - ['scan_qr_popup_closed'], - ['clipboard_text_received', { - req_id: 'request id', - data: 'clipboard value', - }], - ['invoice_closed', { slug: '&&*Sh1j213kx', status: 'PAID' }], - ['phone_requested', { status: 'sent' }], - ['custom_method_invoked', [ - [{ req_id: '1', result: 'My result' }], - [{ req_id: '2', error: 'Something is wrong' }], - ]], - ['write_access_requested', { status: 'allowed' }], - ['unknown_event', [ - ['hello', 'hello'], - [{ there: true }, { there: true }], - ]] as any, - ]; - - testCases.forEach(([event, inputOrCaseOrCases]) => { - it(`should correctly handle "${event}" event data`, () => { - const spy = vi.fn(); - const emitter = createEmitter(); - - emitter.on(event, spy); - - // No expected data to be passed to listener. - if (inputOrCaseOrCases === undefined) { - dispatchWindowMessageEvent(event); - expect(spy).toBeCalledWith(); - return; - } - - // Input is equal to expected result. - if (!Array.isArray(inputOrCaseOrCases)) { - dispatchWindowMessageEvent(event, inputOrCaseOrCases); - expect(spy).toBeCalledWith(inputOrCaseOrCases); - return; - } - - // Input differs from expected result. - if (!Array.isArray(inputOrCaseOrCases[0])) { - const [input, expected] = inputOrCaseOrCases; - dispatchWindowMessageEvent(event, input); - expect(spy).toBeCalledWith(expected); - return; - } - - // List of cases. - inputOrCaseOrCases.forEach(([input, expected = input]) => { - dispatchWindowMessageEvent(event, input); - expect(spy).toBeCalledWith(expected); - }); - }); + emitter.on(event, spy); + + // No expected data to be passed to listener. + if (inputOrCaseOrCases === undefined) { + dispatchWindowMessageEvent(event); + expect(spy).toBeCalledWith(); + return; + } + + // Input is equal to expected result. + if (!Array.isArray(inputOrCaseOrCases)) { + dispatchWindowMessageEvent(event, inputOrCaseOrCases); + expect(spy).toBeCalledWith(inputOrCaseOrCases); + return; + } + + // Input differs from expected result. + if (!Array.isArray(inputOrCaseOrCases[0])) { + const [input, expected] = inputOrCaseOrCases; + dispatchWindowMessageEvent(event, input); + expect(spy).toBeCalledWith(expected); + return; + } + + // List of cases. + inputOrCaseOrCases.forEach(([input, expected = input]) => { + dispatchWindowMessageEvent(event, input); + expect(spy).toBeCalledWith(expected); }); + }); + }); - it('should not emit event in case, it contains incorrect payload', () => { - const spy = vi.fn(); - const emitter = createEmitter(); + it('should not emit event in case, it contains incorrect payload', () => { + const spy = vi.fn(); + const emitter = createEmitter(); - emitter.on('viewport_changed', spy); + emitter.on('viewport_changed', spy); - dispatchWindowMessageEvent('viewport_changed', 'broken data'); + dispatchWindowMessageEvent('viewport_changed', 'broken data'); - expect(spy).not.toBeCalled(); - }); - }); + expect(spy).not.toBeCalled(); }); + }); +}); - describe('singletonEmitter', () => { - it('should return the same instance of emitter', () => { - expect(singletonEmitter()).toEqual(singletonEmitter()); - }); - }); +describe('singletonEmitter', () => { + it('should return the same instance of emitter', () => { + expect(singletonEmitter()).toEqual(singletonEmitter()); }); }); diff --git a/packages/bridge/tests/events/off.ts b/packages/bridge/tests/events/off.ts index 0edb13306..82741ac5c 100644 --- a/packages/bridge/tests/events/off.ts +++ b/packages/bridge/tests/events/off.ts @@ -1,8 +1,8 @@ import { expect, vi, beforeEach, afterEach, describe, it } from 'vitest'; import { on, off } from '../../src/index.js'; -import { createWindow, type WindowSpy } from '../../__test-utils__/createWindow.js'; -import { dispatchWindowMessageEvent } from '../../__test-utils__/dispatchWindowMessageEvent.js'; +import { createWindow, type WindowSpy } from '../../test-utils/createWindow.js'; +import { dispatchWindowMessageEvent } from '../../test-utils/dispatchWindowMessageEvent.js'; let windowSpy: WindowSpy; diff --git a/packages/bridge/tests/events/on.ts b/packages/bridge/tests/events/on.ts index 3ba0ddecd..47df10908 100644 --- a/packages/bridge/tests/events/on.ts +++ b/packages/bridge/tests/events/on.ts @@ -1,8 +1,8 @@ import { expect, it, vi, afterEach, describe, beforeEach } from 'vitest'; import { on } from '../../src/index.js'; -import { createWindow, type WindowSpy } from '../../__test-utils__/createWindow.js'; -import { dispatchWindowMessageEvent } from '../../__test-utils__/dispatchWindowMessageEvent.js'; +import { createWindow, type WindowSpy } from '../../test-utils/createWindow.js'; +import { dispatchWindowMessageEvent } from '../../test-utils/dispatchWindowMessageEvent.js'; let windowSpy: WindowSpy; diff --git a/packages/bridge/tests/events/onTelegramEvent.ts b/packages/bridge/tests/events/onTelegramEvent.ts index 3af2e5dfd..018c7a605 100644 --- a/packages/bridge/tests/events/onTelegramEvent.ts +++ b/packages/bridge/tests/events/onTelegramEvent.ts @@ -1,8 +1,8 @@ import { expect, vi, beforeEach, afterEach, describe, it } from 'vitest'; import { onTelegramEvent } from '../../src/events/onTelegramEvent.js'; -import { createWindow, type WindowSpy } from '../../__test-utils__/createWindow.js'; -import { dispatchWindowMessageEvent } from '../../__test-utils__/dispatchWindowMessageEvent.js'; +import { createWindow, type WindowSpy } from '../../test-utils/createWindow.js'; +import { dispatchWindowMessageEvent } from '../../test-utils/dispatchWindowMessageEvent.js'; let windowSpy: WindowSpy; diff --git a/packages/bridge/tests/events/once.ts b/packages/bridge/tests/events/once.ts index e70e5e92f..6f37bb54b 100644 --- a/packages/bridge/tests/events/once.ts +++ b/packages/bridge/tests/events/once.ts @@ -1,8 +1,8 @@ import { expect, vi, afterEach, describe, it, beforeEach } from 'vitest'; import { once } from '../../src/index.js'; -import { createWindow, type WindowSpy } from '../../__test-utils__/createWindow.js'; -import { dispatchWindowMessageEvent } from '../../__test-utils__/dispatchWindowMessageEvent.js'; +import { createWindow, type WindowSpy } from '../../test-utils/createWindow.js'; +import { dispatchWindowMessageEvent } from '../../test-utils/dispatchWindowMessageEvent.js'; let windowSpy: WindowSpy; diff --git a/packages/bridge/tests/events/subscribe.ts b/packages/bridge/tests/events/subscribe.ts index 5c15cb951..832583226 100644 --- a/packages/bridge/tests/events/subscribe.ts +++ b/packages/bridge/tests/events/subscribe.ts @@ -1,8 +1,8 @@ import { expect, describe, it, vi, beforeEach, afterEach } from 'vitest'; import { subscribe } from '../../src/index.js'; -import { createWindow, type WindowSpy } from '../../__test-utils__/createWindow.js'; -import { dispatchWindowMessageEvent } from '../../__test-utils__/dispatchWindowMessageEvent.js'; +import { createWindow, type WindowSpy } from '../../test-utils/createWindow.js'; +import { dispatchWindowMessageEvent } from '../../test-utils/dispatchWindowMessageEvent.js'; let windowSpy: WindowSpy; diff --git a/packages/bridge/tests/events/unsubscribe.ts b/packages/bridge/tests/events/unsubscribe.ts index c665e6ec9..9dafef10c 100644 --- a/packages/bridge/tests/events/unsubscribe.ts +++ b/packages/bridge/tests/events/unsubscribe.ts @@ -1,8 +1,8 @@ import { expect, it, vi, beforeEach, afterEach, describe } from 'vitest'; import { subscribe, unsubscribe } from '../../src/index.js'; -import { createWindow, type WindowSpy } from '../../__test-utils__/createWindow.js'; -import { dispatchWindowMessageEvent } from '../../__test-utils__/dispatchWindowMessageEvent.js'; +import { createWindow, type WindowSpy } from '../../test-utils/createWindow.js'; +import { dispatchWindowMessageEvent } from '../../test-utils/dispatchWindowMessageEvent.js'; let windowSpy: WindowSpy; diff --git a/packages/bridge/tests/globals.ts b/packages/bridge/tests/globals.ts index aa80036f5..41c10b813 100644 --- a/packages/bridge/tests/globals.ts +++ b/packages/bridge/tests/globals.ts @@ -11,29 +11,29 @@ afterEach(() => { vi.restoreAllMocks(); }); -describe('globals.ts', () => { - describe('logger', () => { - it('should log message in case, debug mode is enabled. Otherwise no output should be shown', () => { - const spy = vi.spyOn(console, 'log').mockImplementation(() => { +describe('logger', () => { + it('should log message in case, debug mode is enabled. Otherwise no output should be shown', () => { + const spy = vi + .spyOn(console, 'log') + .mockImplementation(() => { }); - logger.log(123); - expect(spy).not.toHaveBeenCalled(); + logger.log(123); + expect(spy).not.toHaveBeenCalled(); - setDebug(true); - logger.log('Some log'); - expect(spy).toHaveBeenCalledTimes(1); + setDebug(true); + logger.log('Some log'); + expect(spy).toHaveBeenCalledTimes(1); - setDebug(false); - logger.log('Another log'); - expect(spy).toHaveBeenCalledTimes(1); - }); + setDebug(false); + logger.log('Another log'); + expect(spy).toHaveBeenCalledTimes(1); }); +}); - describe('setTargetOrigin', () => { - it('should return set value via targetOrigin() function', () => { - setTargetOrigin('my test'); - expect(targetOrigin()).toEqual('my test'); - }); +describe('setTargetOrigin', () => { + it('should return set value via targetOrigin() function', () => { + setTargetOrigin('my test'); + expect(targetOrigin()).toEqual('my test'); }); }); diff --git a/packages/bridge/tests/request.ts b/packages/bridge/tests/request.ts index 8566d4dda..274f448e4 100644 --- a/packages/bridge/tests/request.ts +++ b/packages/bridge/tests/request.ts @@ -11,10 +11,10 @@ import { afterAll, } from 'vitest'; -import { createWindow } from '../__test-utils__/createWindow.js'; +import { createWindow } from '../test-utils/createWindow.js'; import { request, type PostEvent } from '../src/index.js'; import { postEvent as globalPostEvent } from '../src/methods/postEvent.js'; -import { dispatchWindowMessageEvent } from '../__test-utils__/dispatchWindowMessageEvent.js'; +import { dispatchWindowMessageEvent } from '../test-utils/dispatchWindowMessageEvent.js'; vi.mock('../src/methods/postEvent.js', async () => { const { postEvent: actualPostEvent } = await vi @@ -47,194 +47,192 @@ function emptyCatch() { } -describe('request.ts', () => { - describe('request', () => { - describe('options', () => { - describe('timeout', () => { - it('should throw an error in case, timeout was reached', () => { - const promise = request('web_app_request_phone', 'phone_requested', { - timeout: 1000, - }); - - vi.advanceTimersByTime(1500); - - return promise.catch(emptyCatch).finally(() => { - expect(promise).rejects.toEqual(new Error('Async call timeout exceeded. Timeout: 1000')); - }); +describe('request', () => { + describe('options', () => { + describe('timeout', () => { + it('should throw an error in case, timeout was reached', () => { + const promise = request('web_app_request_phone', 'phone_requested', { + timeout: 1000, }); - it('should not throw an error in case, data was received before timeout', () => { - const promise = request('web_app_request_phone', 'phone_requested', { - timeout: 1000, - }); - - vi.advanceTimersByTime(500); - dispatchWindowMessageEvent('phone_requested', { status: 'allowed' }); - vi.advanceTimersByTime(1000); + vi.advanceTimersByTime(1500); - return promise.catch(emptyCatch).finally(() => { - expect(promise).resolves.toStrictEqual({ status: 'allowed' }); - }); + return promise.catch(emptyCatch).finally(() => { + expect(promise).rejects.toEqual(new Error('Async call timeout exceeded. Timeout: 1000')); }); }); - describe('postEvent', () => { - it('should use specified postEvent property', () => { - const postEvent = vi.fn(); - request('web_app_request_phone', 'phone_requested', { postEvent }); - expect(postEvent).toHaveBeenCalledWith('web_app_request_phone', undefined); + it('should not throw an error in case, data was received before timeout', () => { + const promise = request('web_app_request_phone', 'phone_requested', { + timeout: 1000, }); - it('should use global postEvent function if according property was not specified', () => { - request('web_app_request_phone', 'phone_requested'); - expect(globalPostEvent).toHaveBeenCalledWith('web_app_request_phone', undefined); - }); + vi.advanceTimersByTime(500); + dispatchWindowMessageEvent('phone_requested', { status: 'allowed' }); + vi.advanceTimersByTime(1000); - it('should reject promise in case, postEvent threw an error', () => { - const promise = request('web_app_request_phone', 'phone_requested', { - postEvent: () => { - throw new Error('Nope!'); - }, - }); - expect(promise).rejects.toStrictEqual(Error('Nope!')); + return promise.catch(emptyCatch).finally(() => { + expect(promise).resolves.toStrictEqual({ status: 'allowed' }); }); }); + }); - describe('capture', () => { - it('should capture an event in case, capture method returned true', () => { - const promise = request('web_app_request_phone', 'phone_requested', { - timeout: 1000, - capture: ({ status }) => status === 'allowed', - }); + describe('postEvent', () => { + it('should use specified postEvent property', () => { + const postEvent = vi.fn(); + request('web_app_request_phone', 'phone_requested', { postEvent }); + expect(postEvent).toHaveBeenCalledWith('web_app_request_phone', undefined); + }); - vi.advanceTimersByTime(500); - dispatchWindowMessageEvent('phone_requested', { status: 'allowed' }); - vi.advanceTimersByTime(1000); + it('should use global postEvent function if according property was not specified', () => { + request('web_app_request_phone', 'phone_requested'); + expect(globalPostEvent).toHaveBeenCalledWith('web_app_request_phone', undefined); + }); - return promise.catch(emptyCatch).finally(() => { - expect(promise).resolves.toStrictEqual({ status: 'allowed' }); - }); + it('should reject promise in case, postEvent threw an error', () => { + const promise = request('web_app_request_phone', 'phone_requested', { + postEvent: () => { + throw new Error('Nope!'); + }, }); + expect(promise).rejects.toStrictEqual(Error('Nope!')); + }); + }); - it('should not capture an event in case, capture method returned false', () => { - const promise = request('web_app_request_phone', 'phone_requested', { - timeout: 500, - capture: ({ status }) => status === 'allowed', - }); + describe('capture', () => { + it('should capture an event in case, capture method returned true', () => { + const promise = request('web_app_request_phone', 'phone_requested', { + timeout: 1000, + capture: ({ status }) => status === 'allowed', + }); - dispatchWindowMessageEvent('phone_requested', { status: 'declined' }); - vi.advanceTimersByTime(1000); + vi.advanceTimersByTime(500); + dispatchWindowMessageEvent('phone_requested', { status: 'allowed' }); + vi.advanceTimersByTime(1000); - return promise.catch(emptyCatch).finally(() => { - expect(promise).rejects.toEqual(new Error('Async call timeout exceeded. Timeout: 500')); - }); + return promise.catch(emptyCatch).finally(() => { + expect(promise).resolves.toStrictEqual({ status: 'allowed' }); }); }); - }); - describe('with request id', () => { - it('should ignore event with the different request id', () => { - const promise = request('web_app_read_text_from_clipboard', { req_id: 'a' }, 'clipboard_text_received', { - timeout: 1000, + it('should not capture an event in case, capture method returned false', () => { + const promise = request('web_app_request_phone', 'phone_requested', { + timeout: 500, + capture: ({ status }) => status === 'allowed', }); - dispatchWindowMessageEvent('clipboard_text_received', { req_id: 'b' }); - vi.advanceTimersByTime(1500); + dispatchWindowMessageEvent('phone_requested', { status: 'declined' }); + vi.advanceTimersByTime(1000); return promise.catch(emptyCatch).finally(() => { - expect(promise).rejects.toEqual(new Error('Async call timeout exceeded. Timeout: 1000')); + expect(promise).rejects.toEqual(new Error('Async call timeout exceeded. Timeout: 500')); }); }); + }); + }); + + describe('with request id', () => { + it('should ignore event with the different request id', () => { + const promise = request('web_app_read_text_from_clipboard', { req_id: 'a' }, 'clipboard_text_received', { + timeout: 1000, + }); - it('should capture event with the same request id', () => { - const promise = request('web_app_read_text_from_clipboard', { req_id: 'a' }, 'clipboard_text_received', { - timeout: 1000, - }); + dispatchWindowMessageEvent('clipboard_text_received', { req_id: 'b' }); + vi.advanceTimersByTime(1500); + + return promise.catch(emptyCatch).finally(() => { + expect(promise).rejects.toEqual(new Error('Async call timeout exceeded. Timeout: 1000')); + }); + }); + + it('should capture event with the same request id', () => { + const promise = request('web_app_read_text_from_clipboard', { req_id: 'a' }, 'clipboard_text_received', { + timeout: 1000, + }); + + dispatchWindowMessageEvent('clipboard_text_received', { + req_id: 'a', + data: 'from clipboard', + }); + vi.advanceTimersByTime(1500); - dispatchWindowMessageEvent('clipboard_text_received', { + return promise.catch(emptyCatch).finally(() => { + expect(promise).resolves.toStrictEqual({ req_id: 'a', data: 'from clipboard', }); + }); + }); + }); + + describe('multiple events', () => { + describe('no params', () => { + it('should handle any of the specified events', () => { + const promise = request( + 'web_app_request_phone', + ['phone_requested', 'write_access_requested'], + { timeout: 1000 }, + ); + const promise2 = request( + 'web_app_request_phone', + ['phone_requested', 'write_access_requested'], + { timeout: 1000 }, + ); + + dispatchWindowMessageEvent('phone_requested', { status: 'allowed' }); + dispatchWindowMessageEvent('write_access_requested', { status: 'declined' }); vi.advanceTimersByTime(1500); - return promise.catch(emptyCatch).finally(() => { - expect(promise).resolves.toStrictEqual({ - req_id: 'a', - data: 'from clipboard', + return Promise + .all([promise, promise2]) + .catch(emptyCatch) + .finally(() => { + expect(promise).resolves.toStrictEqual({ status: 'allowed' }); + expect(promise2).resolves.toStrictEqual({ status: 'declined' }); }); - }); }); }); - describe('multiple events', () => { - describe('no params', () => { - it('should handle any of the specified events', () => { - const promise = request( - 'web_app_request_phone', - ['phone_requested', 'write_access_requested'], - { timeout: 1000 }, - ); - const promise2 = request( - 'web_app_request_phone', - ['phone_requested', 'write_access_requested'], - { timeout: 1000 }, - ); - - dispatchWindowMessageEvent('phone_requested', { status: 'allowed' }); - dispatchWindowMessageEvent('write_access_requested', { status: 'declined' }); - vi.advanceTimersByTime(1500); - - return Promise - .all([promise, promise2]) - .catch(emptyCatch) - .finally(() => { - expect(promise).resolves.toStrictEqual({ status: 'allowed' }); - expect(promise2).resolves.toStrictEqual({ status: 'declined' }); - }); - }); - }); + describe('with params', () => { + it('should handle any of the specified events', () => { + const promise = request( + 'web_app_data_send', + { data: 'abc' }, + ['phone_requested', 'write_access_requested'], + { timeout: 1000 }, + ); + const promise2 = request( + 'web_app_data_send', + { data: 'abc' }, + ['phone_requested', 'write_access_requested'], + { timeout: 1000 }, + ); + + dispatchWindowMessageEvent('phone_requested', { status: 'allowed' }); + dispatchWindowMessageEvent('write_access_requested', { status: 'declined' }); + vi.advanceTimersByTime(1500); - describe('with params', () => { - it('should handle any of the specified events', () => { - const promise = request( - 'web_app_data_send', - { data: 'abc' }, - ['phone_requested', 'write_access_requested'], - { timeout: 1000 }, - ); - const promise2 = request( - 'web_app_data_send', - { data: 'abc' }, - ['phone_requested', 'write_access_requested'], - { timeout: 1000 }, - ); - - dispatchWindowMessageEvent('phone_requested', { status: 'allowed' }); - dispatchWindowMessageEvent('write_access_requested', { status: 'declined' }); - vi.advanceTimersByTime(1500); - - return Promise - .all([promise, promise2]) - .catch(emptyCatch) - .finally(() => { - expect(promise).resolves.toStrictEqual({ status: 'allowed' }); - expect(promise2).resolves.toStrictEqual({ status: 'declined' }); - }); - }); + return Promise + .all([promise, promise2]) + .catch(emptyCatch) + .finally(() => { + expect(promise).resolves.toStrictEqual({ status: 'allowed' }); + expect(promise2).resolves.toStrictEqual({ status: 'declined' }); + }); }); }); - - // it('no params methods', () => { - // it('should properly handle ') - // }); - - // it('with request id', () => { - // const promise = request('web_app_read_text_from_clipboard', { req_id: 'a' }, 'clipboard_text_received'); - // - // dispatchWindowEvent('clipboard_text_received', { req_id: 'b' }); - // expect(promise).resolves.toHaveLength(0); - // dispatchWindowEvent('clipboard_text_received', { req_id: 'a' }); - // expect(promise).resolves.toHaveLength(1); - // }); }); + + // it('no params methods', () => { + // it('should properly handle ') + // }); + + // it('with request id', () => { + // const promise = request('web_app_read_text_from_clipboard', { req_id: 'a' }, 'clipboard_text_received'); + // + // dispatchWindowEvent('clipboard_text_received', { req_id: 'b' }); + // expect(promise).resolves.toHaveLength(0); + // dispatchWindowEvent('clipboard_text_received', { req_id: 'a' }); + // expect(promise).resolves.toHaveLength(1); + // }); }); diff --git a/packages/bridge/tests/supports.ts b/packages/bridge/tests/supports.ts index 08a837958..d8474ee23 100644 --- a/packages/bridge/tests/supports.ts +++ b/packages/bridge/tests/supports.ts @@ -1,42 +1,35 @@ import { describe, expect, it } from 'vitest'; +import type { Version } from '@tma.js/utils'; +import { supports } from '../src/supports.js'; import type { MethodName, - HasCheckSupportMethodName, + HasCheckSupportKeyMethod, HasCheckSupportMethodParam, } from '../src/index.js'; -import { - supports, detectSupportParams, -} from '../src/supports.js'; -type Version = `${number}.${number}`; type HaveCheckSupportMethodTuple = { - [M in HasCheckSupportMethodName]: [M, HasCheckSupportMethodParam] -}[HasCheckSupportMethodName]; - -type Test = [ - version: Version | 'any', - methods: (MethodName | HaveCheckSupportMethodTuple)[], -]; + [M in HasCheckSupportKeyMethod]: [M, HasCheckSupportMethodParam] +}[HasCheckSupportKeyMethod]; /** * Increases specified version by amount of updates. * @param version - initial version. - * @param amount - count of bumps to add. + * @param amount - count of bumps. */ function increaseVersion(version: Version, amount: number): string { - const lastDotIndex = version.lastIndexOf('.'); - const lastPart = parseInt(version.slice(lastDotIndex + 1), 10); - - if (Number.isNaN(lastPart)) { + const match = version.match(/(.+\.)(\d+)$/); + if (!match) { throw new Error(`Invalid version: ${version}`); } - - return `${version.slice(0, lastDotIndex + 1)}${lastPart + amount}`; + return `${match[1]}${parseInt(match[2], 10) + amount}`; } describe('supports', () => { - const tests: Test[] = [ + const tests: [ + version: Version | 'any', + methods: (MethodName | HaveCheckSupportMethodTuple)[], + ][] = [ ['any', [ 'iframe_ready', 'iframe_will_reload', @@ -81,69 +74,52 @@ describe('supports', () => { ]], ]; - tests.forEach(([version, methods]) => { - if (version === 'any') { - methods.forEach((methodOrTuple) => { - if (Array.isArray(methodOrTuple)) { - const [method, param] = methodOrTuple; + tests.forEach(([version, methods]) => { + if (version === 'any') { + methods.forEach((methodOrTuple) => { + if (Array.isArray(methodOrTuple)) { + const [method, param] = methodOrTuple; + it(`should return true in case, passed method is "${method}", parameter is "${param}" and version is 1`, () => { + expect(supports(method, param, '1')).toBe(true); + }); - it(`should return true in case, passed method is "${method}", parameter is "${param}" and version is 1`, () => { - expect(supports(method, param, '1')).toBe(true); - }); - } else { - const method = methodOrTuple; + return; + } - it(`should return true in case, passed method is "${method}" and version is 1`, () => { - expect(supports(method, '1')).toBe(true); - }); - } + const method = methodOrTuple; + it(`should return true in case, passed method is "${method}" and version is 1`, () => { + expect(supports(method, '1')).toBe(true); }); - return; - } + }); + return; + } - methods.forEach((methodOrTuple) => { - if (Array.isArray(methodOrTuple)) { - const [method, param] = methodOrTuple; + methods.forEach((methodOrTuple) => { + if (Array.isArray(methodOrTuple)) { + const [method, param] = methodOrTuple; - it(`should return true in case, passed method is "${method}", parameter is "${param}" and version is ${version} or higher`, () => { - expect(supports(method, param, version)).toBe(true); - expect(supports(method, param, increaseVersion(version, 1))).toBe(true); - }); + it(`should return true in case, passed method is "${method}", parameter is "${param}" and version is ${version} or higher`, () => { + expect(supports(method, param, version)).toBe(true); + expect(supports(method, param, increaseVersion(version, 1))).toBe(true); + }); - it(`should return false in case, passed method is "${method}", parameter is "${param}" and version is lower than ${version}`, () => { - expect(supports(method, param, increaseVersion(version, -1))).toBe(false); - }); - } else { - const method = methodOrTuple; + it(`should return false in case, passed method is "${method}", parameter is "${param}" and version is lower than ${version}`, () => { + expect(supports(method, param, increaseVersion(version, -1))).toBe(false); + }); - it(`should return true in case, passed method is "${method}" and version is ${version} or higher`, () => { - expect(supports(method, version)).toBe(true); - expect(supports(method, increaseVersion(version, 1))).toBe(true); - }); + return; + } - it(`should return false in case, passed method is "${method}" and version is lower than ${version}`, () => { - expect(supports(method, increaseVersion(version, -1))).toBe(false); - }); - } - }); - }); - }); + const method = methodOrTuple; - describe('detectSupportParams', () => { - it('should return ["try_instant_view"] in case, passed method is "web_app_open_link" and params argument contains property "try_instant_view"', () => { - expect( - detectSupportParams('web_app_open_link', { url: '', try_instant_view: true }), - ).toStrictEqual(['try_instant_view']); - expect(detectSupportParams('web_app_open_link', { url: '' })).toStrictEqual([]); - }); + it(`should return true in case, passed method is "${method}" and version is ${version} or higher`, () => { + expect(supports(method, version)).toBe(true); + expect(supports(method, increaseVersion(version, 1))).toBe(true); + }); - it('should return ["color"] in case, passed method is "web_app_set_header_color" and params argument contains property "color"', () => { - expect( - detectSupportParams('web_app_set_header_color', { color: '#abc' }), - ).toStrictEqual(['color']); - expect( - detectSupportParams('web_app_set_header_color', { color_key: 'bg_color' }), - ).toStrictEqual([]); + it(`should return false in case, passed method is "${method}" and version is lower than ${version}`, () => { + expect(supports(method, increaseVersion(version, -1))).toBe(false); + }); }); }); }); From 24fefbe1304ed67cac4dc5f1c67d8b3b4e824240 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Sun, 12 Nov 2023 23:55:32 +0300 Subject: [PATCH 08/13] chore(test-utils): move test utils to a separate dir --- .../bridge/{__test-utils__ => test-utils}/createDomEmitter.ts | 0 packages/bridge/{__test-utils__ => test-utils}/createWindow.ts | 0 .../dispatchWindowMessageEvent.ts | 0 packages/bridge/tsconfig.eslint.json | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename packages/bridge/{__test-utils__ => test-utils}/createDomEmitter.ts (100%) rename packages/bridge/{__test-utils__ => test-utils}/createWindow.ts (100%) rename packages/bridge/{__test-utils__ => test-utils}/dispatchWindowMessageEvent.ts (100%) diff --git a/packages/bridge/__test-utils__/createDomEmitter.ts b/packages/bridge/test-utils/createDomEmitter.ts similarity index 100% rename from packages/bridge/__test-utils__/createDomEmitter.ts rename to packages/bridge/test-utils/createDomEmitter.ts diff --git a/packages/bridge/__test-utils__/createWindow.ts b/packages/bridge/test-utils/createWindow.ts similarity index 100% rename from packages/bridge/__test-utils__/createWindow.ts rename to packages/bridge/test-utils/createWindow.ts diff --git a/packages/bridge/__test-utils__/dispatchWindowMessageEvent.ts b/packages/bridge/test-utils/dispatchWindowMessageEvent.ts similarity index 100% rename from packages/bridge/__test-utils__/dispatchWindowMessageEvent.ts rename to packages/bridge/test-utils/dispatchWindowMessageEvent.ts diff --git a/packages/bridge/tsconfig.eslint.json b/packages/bridge/tsconfig.eslint.json index 0360fb0c4..7b36753de 100644 --- a/packages/bridge/tsconfig.eslint.json +++ b/packages/bridge/tsconfig.eslint.json @@ -3,7 +3,7 @@ "include": [ "src", "tests", - "__test-utils__", + "test-utils", "*.ts", "*.js" ] From a22ed0e564c8c63a91a9c560eaa00a1a3cc84526 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Sun, 12 Nov 2023 23:56:45 +0300 Subject: [PATCH 09/13] feat(events,methods): add web specific methods and events --- apps/docs/apps-communication/events.md | 10 +++++++--- apps/docs/apps-communication/methods.md | 8 ++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/docs/apps-communication/events.md b/apps/docs/apps-communication/events.md index da5507544..92eb49f02 100644 --- a/apps/docs/apps-communication/events.md +++ b/apps/docs/apps-communication/events.md @@ -88,9 +88,9 @@ Available since: **v6.4** Telegram application attempted to extract text from clipboard. -| Field | Type | Description | -|--------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| -| req_id | `string` | Passed during the [web_app_read_text_from_clipboard](methods.md#web-app-read-text-from-clipboard) method invocation `req_id` value. | +| Field | Type | Description | +|--------|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| +| req_id | `string` | Passed during the [web_app_read_text_from_clipboard](methods.md#web-app-read-text-from-clipboard) method invocation `req_id` value. | | data | `string` or `null` | _Optional_. Data extracted from the clipboard. The returned value will have the type `string` only in the case, application has access to the clipboard. | ### `custom_method_invoked` @@ -184,6 +184,10 @@ Application received phone access request status. |-----------|----------|-----------------------------------------------------------------------------------------------------------------------------------------| | button_id | `string` | _Optional_. Identifier of the clicked button. In case, the popup was closed without clicking any button, this property will be omitted. | +### `reload_iframe` + +Parent iframe requested current iframe reload. + ### `qr_text_received` Available since: **v6.4** diff --git a/apps/docs/apps-communication/methods.md b/apps/docs/apps-communication/methods.md index 94e5164ec..c19406da0 100644 --- a/apps/docs/apps-communication/methods.md +++ b/apps/docs/apps-communication/methods.md @@ -89,6 +89,14 @@ Notifies parent iframe about the current frame is ready. This method is only use of Telegram. As a result, Mini App will receive [set_custom_style](events.md#set-custom-style) event. +| Field | Type | Description | +|------------------|-----------|------------------------------------------------------------------| +| reload_supported | `boolean` | _Optional_. True, if current Mini App supports native reloading. | + +### `iframe_will_reload` + +Notifies parent iframe about the current iframe is going to reload. + ### `web_app_close` Closes Mini App. From a15c8edb451d9a1289d6131ab5617b72ad9cec8e Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Mon, 13 Nov 2023 00:09:50 +0300 Subject: [PATCH 10/13] refactor(createpostevent): replace with bridge's function --- .../sdk/src/init/creators/createPostEvent.ts | 36 ------------------- packages/sdk/src/init/creators/index.ts | 1 - packages/sdk/src/init/init.ts | 11 ++++-- 3 files changed, 8 insertions(+), 40 deletions(-) delete mode 100644 packages/sdk/src/init/creators/createPostEvent.ts diff --git a/packages/sdk/src/init/creators/createPostEvent.ts b/packages/sdk/src/init/creators/createPostEvent.ts deleted file mode 100644 index 96081c2d9..000000000 --- a/packages/sdk/src/init/creators/createPostEvent.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { - supports, - postEvent as defaultPostEvent, - detectSupportParams, - type PostEvent, -} from '@tma.js/bridge'; -import { isRecord } from '@tma.js/utils'; - -import { MethodNotSupportedError, ParameterUnsupportedError } from '../../errors/index.js'; - -/** - * Creates postEvent function. - * @param checkCompat - should compatibility check be enabled. - * @param version - platform version. - */ -export function createPostEvent(checkCompat: boolean, version: string): PostEvent { - return checkCompat - ? (method: any, params: any) => { - // Firstly, check if method itself is supported. - if (!supports(method, version)) { - throw new MethodNotSupportedError(method, version); - } - - // Method could use parameters, which are supported only in specific versions of TWA. - if (isRecord(params)) { - detectSupportParams(method, params).forEach((param) => { - if (!supports(method as any, param, version)) { - throw new ParameterUnsupportedError(method, param, version); - } - }); - } - - return defaultPostEvent(method, params); - } - : defaultPostEvent; -} diff --git a/packages/sdk/src/init/creators/index.ts b/packages/sdk/src/init/creators/index.ts index b6e0bf645..6cfd29507 100644 --- a/packages/sdk/src/init/creators/index.ts +++ b/packages/sdk/src/init/creators/index.ts @@ -1,7 +1,6 @@ export * from './createBackButton.js'; export * from './createClosingBehavior.js'; export * from './createMainButton.js'; -export * from './createPostEvent.js'; export * from './createRequestIdGenerator.js'; export * from './createThemeParams.js'; export * from './createViewport.js'; diff --git a/packages/sdk/src/init/init.ts b/packages/sdk/src/init/init.ts index b88c96462..c37922b16 100644 --- a/packages/sdk/src/init/init.ts +++ b/packages/sdk/src/init/init.ts @@ -3,6 +3,8 @@ import { setDebug, setTargetOrigin, on, + createPostEvent, + postEvent as bridgePostEvent, } from '@tma.js/bridge'; import { withTimeout } from '@tma.js/utils'; import { parse, retrieveLaunchData } from '@tma.js/launch-params'; @@ -21,12 +23,13 @@ import { parseCSSVarsOptions, } from './css.js'; import { - createPostEvent, createThemeParams, createBackButton, createMainButton, createViewport, - createWebApp, createRequestIdGenerator, createClosingBehavior, + createWebApp, + createRequestIdGenerator, + createClosingBehavior, } from './creators/index.js'; import type { InitOptions, InitResult } from './types.js'; @@ -76,7 +79,9 @@ async function actualInit(options: InitOptions = {}): Promise { } = lpThemeParams; const createRequestId = createRequestIdGenerator(); - const postEvent = createPostEvent(checkCompat, version); + const postEvent = checkCompat + ? createPostEvent(version) + : bridgePostEvent; const themeParams = createThemeParams(lpThemeParams); const webApp = createWebApp( isPageReload, From b3efa4f03248879c0980284870bbc5bd6fc33e76 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Mon, 13 Nov 2023 00:25:29 +0300 Subject: [PATCH 11/13] feat(bridge): add createPostEvent docs --- .../docs/packages/typescript/tma-js-bridge.md | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/docs/packages/typescript/tma-js-bridge.md b/apps/docs/packages/typescript/tma-js-bridge.md index eb476dc53..29f77c95a 100644 --- a/apps/docs/packages/typescript/tma-js-bridge.md +++ b/apps/docs/packages/typescript/tma-js-bridge.md @@ -16,6 +16,7 @@ utmost level of control over cross-application communication. ## Installation ::: code-group + ```bash [pnpm] pnpm i @tma.js/bridge ``` @@ -27,6 +28,7 @@ npm i @tma.js/bridge ```bash [yarn] yarn add @tma.js/bridge ``` + ::: ## Calling methods @@ -111,11 +113,42 @@ supports('web_app_trigger_haptic_feedback', '6.0'); // false supports('web_app_trigger_haptic_feedback', '6.1'); // true ``` +The `supports` function also allows checking if specified parameter in method parameters is +supported: + +```typescript +import { supports } from '@tma.js/bridge'; + +supports('web_app_open_link', 'try_instant_view', '6.0'); // false +supports('web_app_open_link', 'try_instant_view', '6.7'); // true +``` + ::: tip -It is recommended to use this function before calling Telegram Mini Apps methods to prevent +It is recommended to use this function before calling Telegram Mini Apps methods to prevent applications from stalling and other unexpected behavior. ::: +### Creating safer `postEvent` + +This package includes a function named `createPostEvent` that takes the current Telegram Mini Apps +version as input. It returns the `postEvent` function, which internally checks if the specified +method and parameters are supported. If they are not, the function will throw an error. + +```typescript +import { createPostEvent } from '@tma.js/bridge'; + +const postEvent = createPostEvent('6.5'); + +// Will work fine. +postEvent('web_app_read_text_from_clipboard'); + +// Will throw an error. +postEvent('web_app_request_phone'); +``` + +It is highly recommended to use this `postEvent` generator to ensure that method calls work as +expected. + ## Debugging Package supports enabling the debug mode, which leads to logging From 57f0b232f4c5ba60777624925188eb46a2ac1952 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Mon, 13 Nov 2023 00:34:46 +0300 Subject: [PATCH 12/13] docs(changeset): - Support `reload_iframe` event, `iframe_will_reload` method and `iframe_ready` `reload_supported` parameter - Implement `createPostEvent` method - Add error classes - Add new theme parameters --- .changeset/perfect-jeans-turn.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/perfect-jeans-turn.md diff --git a/.changeset/perfect-jeans-turn.md b/.changeset/perfect-jeans-turn.md new file mode 100644 index 000000000..4947feb40 --- /dev/null +++ b/.changeset/perfect-jeans-turn.md @@ -0,0 +1,8 @@ +--- +"@tma.js/bridge": minor +--- + +- Support `reload_iframe` event, `iframe_will_reload` method and `iframe_ready` `reload_supported` parameter +- Implement `createPostEvent` method +- Add error classes +- Add new theme parameters From 79ef697fe49ac791de9be32aadbbcf373f493caf Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Mon, 13 Nov 2023 00:56:06 +0300 Subject: [PATCH 13/13] fix(types): fix incorrect types --- packages/bridge/src/methods/methods.ts | 16 ++++++++-------- packages/bridge/src/supports.ts | 8 ++++---- packages/bridge/tests/supports.ts | 8 ++++---- packages/sdk/src/supports.ts | 8 ++++---- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/bridge/src/methods/methods.ts b/packages/bridge/src/methods/methods.ts index 2b91a6fb3..2a797f6fb 100644 --- a/packages/bridge/src/methods/methods.ts +++ b/packages/bridge/src/methods/methods.ts @@ -16,9 +16,9 @@ export type HeaderColorKey = 'bg_color' | 'secondary_bg_color'; */ export type SwitchInlineQueryChatType = 'users' | 'bots' | 'groups' | 'channels'; -interface CreateParams = never> { +interface CreateParams = never> { params: Params; - supportCheckKey: SupportCheckKey; + versionedParams: VersionedParam; } /** @@ -360,14 +360,14 @@ export type NonEmptyMethodName = { }[MethodName]; /** - * Method names which could be used in supportsParam method. + * Method names which have versioned params. */ -export type HasCheckSupportKeyMethod = { - [M in MethodName]: IsNever extends true ? never : M; +export type MethodWithVersionedParams = { + [M in MethodName]: IsNever extends true ? never : M; }[MethodName]; /** - * Method parameter which can be checked via support method. + * Method parameters which appear only in the specific Telegram Mini Apps version. */ -export type HasCheckSupportMethodParam = - Methods[M]['supportCheckKey']; +export type MethodVersionedParams = + Methods[M]['versionedParams']; diff --git a/packages/bridge/src/supports.ts b/packages/bridge/src/supports.ts index 01aa3fc95..65234f223 100644 --- a/packages/bridge/src/supports.ts +++ b/packages/bridge/src/supports.ts @@ -1,8 +1,8 @@ import { compareVersions, type Version } from '@tma.js/utils'; import type { - HasCheckSupportMethodParam, - HasCheckSupportKeyMethod, + MethodVersionedParams, + MethodWithVersionedParams, MethodName, } from './methods/index.js'; @@ -21,9 +21,9 @@ function versionLessOrEqual(a: Version, b: Version): boolean { * @param param - method parameter * @param inVersion - platform version. */ -export function supports( +export function supports( method: M, - param: HasCheckSupportMethodParam, + param: MethodVersionedParams, inVersion: Version, ): boolean; diff --git a/packages/bridge/tests/supports.ts b/packages/bridge/tests/supports.ts index d8474ee23..fd5ac58c8 100644 --- a/packages/bridge/tests/supports.ts +++ b/packages/bridge/tests/supports.ts @@ -4,13 +4,13 @@ import type { Version } from '@tma.js/utils'; import { supports } from '../src/supports.js'; import type { MethodName, - HasCheckSupportKeyMethod, - HasCheckSupportMethodParam, + MethodWithVersionedParams, + MethodVersionedParams, } from '../src/index.js'; type HaveCheckSupportMethodTuple = { - [M in HasCheckSupportKeyMethod]: [M, HasCheckSupportMethodParam] -}[HasCheckSupportKeyMethod]; + [M in MethodWithVersionedParams]: [M, MethodVersionedParams] +}[MethodWithVersionedParams]; /** * Increases specified version by amount of updates. diff --git a/packages/sdk/src/supports.ts b/packages/sdk/src/supports.ts index 0bb980b10..26bb00598 100644 --- a/packages/sdk/src/supports.ts +++ b/packages/sdk/src/supports.ts @@ -2,15 +2,15 @@ import type { Version } from '@tma.js/utils'; import { supports, type MethodName, - type HasCheckSupportMethodName, - type HasCheckSupportMethodParam, + type MethodVersionedParams, + type MethodWithVersionedParams, } from '@tma.js/bridge'; export type SupportsFunc = (method: M) => boolean; type HasCheckSupportMethodTuple = { - [M in HasCheckSupportMethodName]: [M, HasCheckSupportMethodParam] -}[HasCheckSupportMethodName]; + [M in MethodWithVersionedParams]: [M, MethodVersionedParams] +}[MethodWithVersionedParams]; /** * Returns function, which accepts predefined method name and checks if it is supported