From 879df4860b7d1b7890abbfaaa7cfe5d6cbed1857 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:30:34 +0300 Subject: [PATCH 01/20] feat(bridge): add one more value to PhoneRequestedStatus type --- packages/sdk/src/bridge/events/parsers/phoneRequested.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/bridge/events/parsers/phoneRequested.ts b/packages/sdk/src/bridge/events/parsers/phoneRequested.ts index eb68c18bf..52182ba11 100644 --- a/packages/sdk/src/bridge/events/parsers/phoneRequested.ts +++ b/packages/sdk/src/bridge/events/parsers/phoneRequested.ts @@ -1,6 +1,6 @@ import { json, string } from '~/parsing/index.js'; -export type PhoneRequestedStatus = 'sent' | string; +export type PhoneRequestedStatus = 'sent' | 'cancelled' | string; export interface PhoneRequestedPayload { /** From 9e2c9faf7479ef6994446cedc84033a70e30afca Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:31:21 +0300 Subject: [PATCH 02/20] refactor(bridge): rework custom methods --- .../sdk/src/bridge/methods/custom-methods.ts | 68 +++++++++++++++++++ packages/sdk/src/bridge/methods/index.ts | 2 +- .../bridge/methods/invoke-custom-method.ts | 25 ------- packages/sdk/src/bridge/methods/methods.ts | 8 +-- 4 files changed, 72 insertions(+), 31 deletions(-) create mode 100644 packages/sdk/src/bridge/methods/custom-methods.ts delete mode 100644 packages/sdk/src/bridge/methods/invoke-custom-method.ts diff --git a/packages/sdk/src/bridge/methods/custom-methods.ts b/packages/sdk/src/bridge/methods/custom-methods.ts new file mode 100644 index 000000000..22fc3185a --- /dev/null +++ b/packages/sdk/src/bridge/methods/custom-methods.ts @@ -0,0 +1,68 @@ +import type { RequestId } from '~/types/index.js'; + +interface CreateInvokeCustomMethodParams { + /** + * Unique request identifier. + */ + req_id: RequestId; + + /** + * Method name. + */ + method: M; + + /** + * Method specific parameters. + */ + params: Params; +} + +export interface CustomMethodsParams { + /** + * Deletes storage values by their keys. + */ + deleteStorageValues: { + keys: string | string[] + }; + + /** + * Gets current user contact in case, Mini has access to it. + */ + getRequestedContact: {}; + + /** + * Gets all registered storage keys. + */ + getStorageKeys: {}; + + /** + * Gets storage values by their keys. + */ + getStorageValues: { + keys: string | string[] + }; + + /** + * Saves value by specified key in the storage. + */ + saveStorageValue: { + key: string; + value: string; + }; +} + +/** + * Known custom method name. + */ +export type CustomMethodName = keyof CustomMethodsParams; + +/** + * Custom method parameters. + */ +export type CustomMethodParams = CustomMethodsParams[M]; + +export type AnyInvokeCustomMethodParams = + | CreateInvokeCustomMethodParams + | { + [M in CustomMethodName]: CreateInvokeCustomMethodParams> +}[CustomMethodName]; diff --git a/packages/sdk/src/bridge/methods/index.ts b/packages/sdk/src/bridge/methods/index.ts index f60300ca3..f4f00991e 100644 --- a/packages/sdk/src/bridge/methods/index.ts +++ b/packages/sdk/src/bridge/methods/index.ts @@ -1,6 +1,6 @@ export * from './createPostEvent.js'; +export * from './custom-methods.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/sdk/src/bridge/methods/invoke-custom-method.ts b/packages/sdk/src/bridge/methods/invoke-custom-method.ts deleted file mode 100644 index 1a3f434de..000000000 --- a/packages/sdk/src/bridge/methods/invoke-custom-method.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { RequestId } from '~/types/index.js'; - -interface CreateInvokeCustomMethodParams { - /** - * Unique request identifier. - */ - req_id: RequestId; - - /** - * Method name. - */ - method: M; - - /** - * Method specific parameters. - */ - params: Params; -} - -export type AnyInvokeCustomMethodParams = - | CreateInvokeCustomMethodParams<'deleteStorageValues', { keys: string | string[] }> - | CreateInvokeCustomMethodParams<'getStorageValues', { keys: string | string[] }> - | CreateInvokeCustomMethodParams<'getStorageKeys', {}> - | CreateInvokeCustomMethodParams<'saveStorageValue', { key: string, value: string }> - | CreateInvokeCustomMethodParams; diff --git a/packages/sdk/src/bridge/methods/methods.ts b/packages/sdk/src/bridge/methods/methods.ts index 7a5e9e2e7..4930a0335 100644 --- a/packages/sdk/src/bridge/methods/methods.ts +++ b/packages/sdk/src/bridge/methods/methods.ts @@ -1,8 +1,8 @@ import type { RGB } from '~/colors/index.js'; import type { IsNever, Not, RequestId, UnionKeys } from '~/types/index.js'; +import type { AnyInvokeCustomMethodParams } from './custom-methods.js'; import type { AnyHapticFeedbackParams } from './haptic.js'; -import type { AnyInvokeCustomMethodParams } from './invoke-custom-method.js'; import type { PopupParams } from './popup.js'; /** @@ -78,7 +78,6 @@ export interface MiniAppsMethods { */ web_app_expand: CreateParams; - // TODO: 'getRequestedContact'. https://telegram.org/js/telegram-web-app.js /** * Invokes custom method. * @since v6.9 @@ -173,7 +172,6 @@ export interface MiniAppsMethods { */ web_app_ready: CreateParams; - // TODO: Check if it is right. It probably requests other user phone. /** * Requests access to current user's phone. * @since v6.9 @@ -219,13 +217,13 @@ export interface MiniAppsMethods { * @see https://docs.telegram-mini-apps.com/platform/apps-communication/methods#web-app-set-header-color */ web_app_set_header_color: CreateParams< - | { + | { /** * The Mini App header color key. */ color_key: HeaderColorKey } - | { + | { /** * Color in RGB format. * @since v6.9 From 09a1143be6564d39a5c61ab1dd94269e71855017 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:31:59 +0300 Subject: [PATCH 03/20] chore(types): add Mini Apps method execution options --- packages/sdk/src/types/index.ts | 1 + packages/sdk/src/types/methods.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 packages/sdk/src/types/methods.ts diff --git a/packages/sdk/src/types/index.ts b/packages/sdk/src/types/index.ts index 9d1f423ac..e1945f0b6 100644 --- a/packages/sdk/src/types/index.ts +++ b/packages/sdk/src/types/index.ts @@ -1,3 +1,4 @@ +export * from './methods.js'; export * from './platform.js'; export * from './request-id.js'; export * from './utils.js'; diff --git a/packages/sdk/src/types/methods.ts b/packages/sdk/src/types/methods.ts new file mode 100644 index 000000000..4b11e43a9 --- /dev/null +++ b/packages/sdk/src/types/methods.ts @@ -0,0 +1,18 @@ +import type { PostEvent } from '~/bridge/index.js'; + +export interface ExecuteWithTimeout { + /** + * Timeout to execute method. + */ + timeout?: number; +} + +export interface ExecuteWithPostEvent { + /** + * postEvent function to use to call Telegram Mini Apps methods. + */ + postEvent?: PostEvent; +} + +export interface ExecuteWithOptions extends ExecuteWithTimeout, ExecuteWithPostEvent { +} From 265c7e573899639b09c180b9818d6fa4a92bb490 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:32:21 +0300 Subject: [PATCH 04/20] feat(sdk): implement "sleep" utility --- packages/sdk/src/timeout/sleep.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/sdk/src/timeout/sleep.ts diff --git a/packages/sdk/src/timeout/sleep.ts b/packages/sdk/src/timeout/sleep.ts new file mode 100644 index 000000000..31ed54509 --- /dev/null +++ b/packages/sdk/src/timeout/sleep.ts @@ -0,0 +1,10 @@ +/** + * Awaits for specified amount of time. + * @param duration - duration to await. + */ +export function sleep(duration: number): Promise { + // eslint-disable-next-line no-await-in-loop,@typescript-eslint/no-loop-func + return new Promise((res) => { + setTimeout(res, duration); + }); +} From 67af11f9ecce895525515ca674b5727193380545 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:37:36 +0300 Subject: [PATCH 05/20] refactor(withTimeout): refactor withTimeout function and accept only function which returns promise --- packages/sdk/src/timeout/index.ts | 1 + packages/sdk/src/timeout/withTimeout.ts | 32 ++++++++----------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/packages/sdk/src/timeout/index.ts b/packages/sdk/src/timeout/index.ts index dcd542a76..217eb372f 100644 --- a/packages/sdk/src/timeout/index.ts +++ b/packages/sdk/src/timeout/index.ts @@ -1,3 +1,4 @@ export * from './isTimeoutError.js'; +export * from './sleep.js'; export * from './TimeoutError.js'; export * from './withTimeout.js'; diff --git a/packages/sdk/src/timeout/withTimeout.ts b/packages/sdk/src/timeout/withTimeout.ts index e3ee39739..b8c191d4b 100644 --- a/packages/sdk/src/timeout/withTimeout.ts +++ b/packages/sdk/src/timeout/withTimeout.ts @@ -1,36 +1,24 @@ import { TimeoutError } from './TimeoutError.js'; -type AnyAsyncFunction = (...args: any[]) => Promise; - /** * Creates promise which rejects after timeout milliseconds. * @param timeout - timeout in milliseconds. */ -function createTimeoutPromise(timeout: number): Promise { +function createTimeoutPromise(timeout: number): Promise { return new Promise((_, rej) => { setTimeout(rej, timeout, new TimeoutError(timeout)); }); } /** - * Rejects specified promise in case, it is processed more than timeout seconds. - * @param promise - wrapped promise. - * @param timeout - timeout in milliseconds. + * Accepts specified function and instantly executes. It waits for timeout milliseconds for + * it to complete and throws an error in case, deadline was reached. + * @param func - function to execute. + * @param timeout - completion timeout. */ -export function withTimeout

>(promise: P, timeout: number): P; -/** - * Wraps async function in function using timeout. - * @param func - wrapped function. - * @param timeout - async function timeout. - */ -export function withTimeout(func: F, timeout: number): F; -export function withTimeout(funcOrPromise: Promise | AnyAsyncFunction, timeout: number) { - if (typeof funcOrPromise === 'function') { - return (...args: any[]) => Promise.race([ - funcOrPromise(...args), - createTimeoutPromise(timeout), - ]); - } - - return Promise.race([funcOrPromise, createTimeoutPromise(timeout)]); +export function withTimeout(func: () => Promise, timeout: number): Promise { + return Promise.race([ + func(), + createTimeoutPromise(timeout), + ]); } From f198185bf806838df0053c5f36563da70a1b3146 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:38:02 +0300 Subject: [PATCH 06/20] feat(bridge): implement invokeCustomMethod function --- packages/sdk/src/bridge/index.ts | 1 + .../sdk/src/bridge/invoke-custom-method.ts | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 packages/sdk/src/bridge/invoke-custom-method.ts diff --git a/packages/sdk/src/bridge/index.ts b/packages/sdk/src/bridge/index.ts index e0e2b472d..085f7cb2c 100644 --- a/packages/sdk/src/bridge/index.ts +++ b/packages/sdk/src/bridge/index.ts @@ -2,4 +2,5 @@ export * from './env/index.js'; export * from './errors/index.js'; export * from './events/index.js'; export * from './methods/index.js'; +export * from './invoke-custom-method.js'; export * from './request.js'; diff --git a/packages/sdk/src/bridge/invoke-custom-method.ts b/packages/sdk/src/bridge/invoke-custom-method.ts new file mode 100644 index 000000000..2b47ebf06 --- /dev/null +++ b/packages/sdk/src/bridge/invoke-custom-method.ts @@ -0,0 +1,56 @@ +import type { ExecuteWithOptions } from '~/types/index.js'; + +import type { CustomMethodName, CustomMethodParams } from './methods/index.js'; +import { request } from './request.js'; + +/** + * Invokes known custom method. Returns method execution result. + * @param method - method name. + * @param params - method parameters. + * @param requestId - request identifier. + * @param options - additional options. + */ +export async function invokeCustomMethod( + method: M, + params: CustomMethodParams, + requestId: string, + options?: ExecuteWithOptions, +): Promise; + +/** + * Invokes unknown custom method. Returns method execution result. + * @param method - method name. + * @param params - method parameters. + * @param requestId - request identifier. + * @param options - additional options. + */ +export function invokeCustomMethod( + method: string, + params: object, + requestId: string, + options?: ExecuteWithOptions, +): Promise; + +export async function invokeCustomMethod( + method: string, + params: object, + requestId: string, + options: ExecuteWithOptions = {}, +): Promise { + const { result, error } = await request( + 'web_app_invoke_custom_method', + { + method, + params, + req_id: requestId, + }, + 'custom_method_invoked', + options, + ); + + if (error) { + throw new Error(error); + } + + return result; +} From 4f4f175bfe775e6a7119286a0da23dc31bd8adc1 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:39:04 +0300 Subject: [PATCH 07/20] refactor(bridge): refactor request function --- packages/sdk/src/bridge/request.ts | 79 +++++++++++++----------------- 1 file changed, 35 insertions(+), 44 deletions(-) diff --git a/packages/sdk/src/bridge/request.ts b/packages/sdk/src/bridge/request.ts index 4d54dd019..131fd73ca 100644 --- a/packages/sdk/src/bridge/request.ts +++ b/packages/sdk/src/bridge/request.ts @@ -1,6 +1,6 @@ import { isRecord } from '~/misc/index.js'; import { withTimeout } from '~/timeout/index.js'; -import type { And, If, IsNever } from '~/types/index.js'; +import type { And, ExecuteWithOptions, If, IsNever } from '~/types/index.js'; import { type MiniAppsEventHasParams, @@ -14,7 +14,6 @@ import { type MiniAppsMethodName, type MiniAppsMethodParams, type MiniAppsNonEmptyMethodName, - type PostEvent, postEvent as defaultPostEvent, } from './methods/index.js'; @@ -45,17 +44,7 @@ type EventWithRequestId = { >; }[MiniAppsEventName]; -export interface RequestOptions { - /** - * Bridge postEvent method. - * @default Global postEvent method. - */ - postEvent?: PostEvent; - - /** - * Execution timeout. - */ - timeout?: number; +export interface RequestOptions extends ExecuteWithOptions { } export interface RequestOptionsAdvanced extends RequestOptions { @@ -149,37 +138,39 @@ export function request( ? executionOptions.capture : null; - const promise = new Promise((res, rej) => { - // Iterate over each event and create event listener. - const stoppers = events.map((ev) => on(ev, (data?) => { - // If request identifier was specified, we are waiting for event with the same value - // to occur. - if (typeof requestId === 'string' && (!isRecord(data) || data.req_id !== requestId)) { - return; - } - - if (typeof capture === 'function' && !capture(data)) { - return; + const execute = () => { + return new Promise((res, rej) => { + // Iterate over each event and create event listener. + const stoppers = events.map((ev) => on(ev, (data?) => { + // If request identifier was specified, we are waiting for event with the same value + // to occur. + if (requestId && (!isRecord(data) || data.req_id !== requestId)) { + return; + } + + if (typeof capture === 'function' && !capture(data)) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-use-before-define + stopListening(); + res(data); + })); + + // Function which removes all event listeners. + const stopListening = () => stoppers.forEach((stop) => stop()); + + try { + // We are wrapping this call in try catch, because it can throw errors in case, + // compatibility check was enabled. We want an error to be captured by promise, not by + // another one external try catch. + postEvent(method as any, methodParams); + } catch (e) { + stopListening(); + rej(e); } + }); + }; - // eslint-disable-next-line @typescript-eslint/no-use-before-define - stopListening(); - res(data); - })); - - // Function which removes all event listeners. - const stopListening = () => stoppers.forEach((stop) => stop()); - - try { - // We are wrapping this call in try catch, because it can throw errors in case, - // compatibility check was enabled. We want an error to be captured by promise, not by - // another one external try catch. - postEvent(method as any, methodParams); - } catch (e) { - stopListening(); - rej(e); - } - }); - - return typeof timeout === 'number' ? withTimeout(promise, timeout) : promise; + return typeof timeout === 'number' ? withTimeout(execute, timeout) : execute(); } From 67037088de3603fa0d79536a05dc3aa1a2677eec Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:39:24 +0300 Subject: [PATCH 08/20] refactor(cloud-storage): refactor CloudStorage methods --- .../sdk/src/cloud-storage/CloudStorage.ts | 89 ++++++++----------- 1 file changed, 36 insertions(+), 53 deletions(-) diff --git a/packages/sdk/src/cloud-storage/CloudStorage.ts b/packages/sdk/src/cloud-storage/CloudStorage.ts index db5eccbd1..20b7674a3 100644 --- a/packages/sdk/src/cloud-storage/CloudStorage.ts +++ b/packages/sdk/src/cloud-storage/CloudStorage.ts @@ -1,7 +1,6 @@ import { + invokeCustomMethod, postEvent as defaultPostEvent, - request, - type RequestOptions, } from '~/bridge/index.js'; import { array, @@ -12,18 +11,9 @@ import { createSupportsFunc, type SupportsFunc, } from '~/supports/index.js'; -import type { CreateRequestIdFunc } from '~/types/index.js'; +import type { CreateRequestIdFunc, ExecuteWithTimeout } from '~/types/index.js'; import type { Version } from '~/version/index.js'; -type WiredRequestOptions = Omit; - -interface Methods { - deleteStorageValues: { keys: string | string[] }; - getStorageValues: { keys: string | string[] }; - getStorageKeys: {}; - saveStorageValue: { key: string; value: string }; -} - function objectFromKeys(keys: K[], value: V): Record { return keys.reduce>((acc, key) => { acc[key] = value; @@ -45,51 +35,36 @@ export class CloudStorage { }); } - /** - * Invokes custom method related to CloudStorage. - * @param method - method name. - * @param params - method parameters. - * @param options - execution options. - */ - private async invokeCustomMethod( - method: M, - params: Methods[M], - options: WiredRequestOptions = {}, - ): Promise { - const { result, error } = await request( - 'web_app_invoke_custom_method', - { method, params, req_id: this.createRequestId() }, - 'custom_method_invoked', - { ...options, postEvent: this.postEvent }, - ); - - if (error) { - throw new Error(error); - } - - return result; - } - /** * Deletes specified key or keys from the cloud storage. * @param keyOrKeys - key or keys to delete. * @param options - request execution options. */ - async delete(keyOrKeys: string | string[], options?: WiredRequestOptions): Promise { + async delete(keyOrKeys: string | string[], options: ExecuteWithTimeout = {}): Promise { const keys = Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]; if (keys.length === 0) { return; } - await this.invokeCustomMethod('deleteStorageValues', { keys }, options); + await invokeCustomMethod( + 'deleteStorageValues', + { keys }, + this.createRequestId(), + { ...options, postEvent: this.postEvent }, + ); } /** * Returns list of all keys presented in the cloud storage. * @param options - request execution options. */ - async getKeys(options?: WiredRequestOptions): Promise { - const result = await this.invokeCustomMethod('getStorageKeys', {}, options); + async getKeys(options: ExecuteWithTimeout = {}): Promise { + const result = await invokeCustomMethod( + 'getStorageKeys', + {}, + this.createRequestId(), + { ...options, postEvent: this.postEvent }, + ); return array().of(string()).parse(result); } @@ -102,7 +77,7 @@ export class CloudStorage { */ get( keys: K[], - options?: WiredRequestOptions, + options?: ExecuteWithTimeout, ): Promise>; /** @@ -112,11 +87,11 @@ export class CloudStorage { * @return Value of the specified key. In case, key was not created previously, function * will return empty string. */ - get(key: string, options?: WiredRequestOptions): Promise; + get(key: string, options?: ExecuteWithTimeout): Promise; async get( keyOrKeys: string | string[], - options?: WiredRequestOptions, + options: ExecuteWithTimeout = {}, ): Promise> { const keys = Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]; if (keys.length === 0) { @@ -126,9 +101,12 @@ export class CloudStorage { const schema = json( objectFromKeys(keys, string()), ); - const result = await this - .invokeCustomMethod('getStorageValues', { keys }, options) - .then((data) => schema.parse(data)); + const result = await invokeCustomMethod( + 'getStorageValues', + { keys }, + this.createRequestId(), + { ...options, postEvent: this.postEvent }, + ).then((data) => schema.parse(data)); return Array.isArray(keyOrKeys) ? result : result[keyOrKeys]; } @@ -139,17 +117,22 @@ export class CloudStorage { * @param value - storage value. * @param options - request execution options. */ - async set(key: string, value: string, options?: WiredRequestOptions): Promise { - await this.invokeCustomMethod('saveStorageValue', { key, value }, options); + async set(key: string, value: string, options: ExecuteWithTimeout = {}): Promise { + await invokeCustomMethod( + 'saveStorageValue', + { key, value }, + this.createRequestId(), + { ...options, postEvent: this.postEvent }, + ); } /** * Checks if specified method is supported by current component. */ supports: SupportsFunc< - | 'delete' - | 'get' - | 'getKeys' - | 'set' + | 'delete' + | 'get' + | 'getKeys' + | 'set' >; } From fc516119e39d0fd44eb98b66253a962db20318a9 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:40:50 +0300 Subject: [PATCH 09/20] feat(mini-app): implement requestContact method. Rework requestPhoneAccess and requestWriteAccess methods --- .../sdk/src/init/creators/createMiniApp.ts | 4 + packages/sdk/src/init/init.ts | 1 + packages/sdk/src/mini-app/MiniApp.ts | 151 +++++++++++++++--- packages/sdk/src/mini-app/contactParser.ts | 29 ++++ packages/sdk/src/mini-app/types.ts | 13 ++ 5 files changed, 178 insertions(+), 20 deletions(-) create mode 100644 packages/sdk/src/mini-app/contactParser.ts diff --git a/packages/sdk/src/init/creators/createMiniApp.ts b/packages/sdk/src/init/creators/createMiniApp.ts index d046816f5..64204e159 100644 --- a/packages/sdk/src/init/creators/createMiniApp.ts +++ b/packages/sdk/src/init/creators/createMiniApp.ts @@ -2,6 +2,7 @@ import { MiniApp } from '~/mini-app/index.js'; import { getStorageValue, saveStorageValue } from '~/storage.js'; import type { PostEvent } from '~/bridge/index.js'; import type { RGB } from '~/colors/index.js'; +import type { CreateRequestIdFunc } from '~/types/index.js'; import type { Version } from '~/version/index.js'; /** @@ -11,6 +12,7 @@ import type { Version } from '~/version/index.js'; * @param backgroundColor - web app background color. * @param version - platform version. * @param botInline - is Mini App launched in inline mode. + * @param createRequestId - function which generates request identifiers. * @param postEvent - Bridge postEvent function */ export function createMiniApp( @@ -18,6 +20,7 @@ export function createMiniApp( backgroundColor: RGB, version: Version, botInline: boolean, + createRequestId: CreateRequestIdFunc, postEvent: PostEvent, ): MiniApp { const { @@ -30,6 +33,7 @@ export function createMiniApp( backgroundColor: stateBackgroundColor, version, botInline, + createRequestId, postEvent, }); diff --git a/packages/sdk/src/init/init.ts b/packages/sdk/src/init/init.ts index 28307d0c4..1d0108b72 100644 --- a/packages/sdk/src/init/init.ts +++ b/packages/sdk/src/init/init.ts @@ -81,6 +81,7 @@ export function init(options: InitOptions = {}): InitResult | Promise { if (!isSupported(method)) { return false; @@ -66,7 +78,6 @@ export class MiniApp { if (method === 'switchInlineQuery' && !botInline) { return false; } - return true; }; @@ -76,6 +87,22 @@ export class MiniApp { }); } + /** + * Attempts to get requested contact. + */ + private async getRequestedContact(): Promise { + return invokeCustomMethod( + 'getRequestedContact', + {}, + this.createRequestId(), + { + postEvent: this.postEvent, + timeout: 10000, + }, + ) + .then((data) => contactParser.parse(data)); + } + /** * The Mini App background color. */ @@ -111,6 +138,20 @@ export class MiniApp { return isColorDark(this.backgroundColor); } + /** + * True if phone access is currently being requested. + */ + get isRequestingPhoneAccess(): boolean { + return this.requestingPhoneAccess; + } + + /** + * True if write access is currently being requested. + */ + get isRequestingWriteAccess(): boolean { + return this.requestingWriteAccess; + } + /** * Adds new event listener. */ @@ -135,25 +176,95 @@ export class MiniApp { } /** - * Requests current user phone access. + * Requests current user contact information. In contrary to requestPhoneAccess, this method + * returns promise with contact information that rejects in case, user denied access, or request + * failed. + * @param options - additional options. */ - requestPhoneAccess(): Promise { - return request( - 'web_app_request_phone', - 'phone_requested', - { postEvent: this.postEvent }, - ).then((data) => data.status); + async requestContact({ timeout = 5000 }: ExecuteWithTimeout = {}): Promise { + // First of all, let's try to get the requested contact. Probably, we already requested + // it before. + try { + return await this.getRequestedContact(); + } catch (e) { /* empty */ + } + + // Then, request access to user's phone. + const status = await this.requestPhoneAccess(); + if (status !== 'sent') { + throw new Error('Access denied.'); + } + + // Expected deadline. + const deadlineAt = Date.now() + timeout; + + // Time to wait before executing the next request. + let sleepTime = 50; + + return withTimeout(async () => { + // We are trying to retrieve the requested contact until deadline was reached. + while (Date.now() < deadlineAt) { + try { + // eslint-disable-next-line no-await-in-loop + return await this.getRequestedContact(); + } catch (e) { /* empty */ + } + + // Sleep for some time. + // eslint-disable-next-line no-await-in-loop + await sleep(sleepTime); + + // Increase the sleep time not to kill the backend service. + sleepTime += 50; + } + + throw new Error('Unable to retrieve requested contact.'); + }, timeout); + } + + /** + * Requests current user phone access. Method returns promise, which resolves + * status of the request. In case, user accepted the request, Mini App bot will receive + * the according notification. + * + * To obtain the retrieved information instead, utilize the requestContact method. + * @param options - additional options. + * @see requestContact + */ + requestPhoneAccess(options: ExecuteWithTimeout = {}): Promise { + if (this.requestingPhoneAccess) { + throw new Error('Phone access is already being requested.'); + } + this.requestingPhoneAccess = true; + + return request('web_app_request_phone', 'phone_requested', { + ...options, + postEvent: this.postEvent, + }) + .then((data) => data.status) + .finally(() => { + this.requestingPhoneAccess = false; + }); } /** * Requests write message access to current user. + * @param options - additional options. */ - requestWriteAccess(): Promise { - return request( - 'web_app_request_write_access', - 'write_access_requested', - { postEvent: this.postEvent }, - ).then((data) => data.status); + requestWriteAccess(options: ExecuteWithTimeout = {}): Promise { + if (this.requestingWriteAccess) { + throw new Error('Write access is already being requested.'); + } + this.requestingWriteAccess = true; + + return request('web_app_request_write_access', 'write_access_requested', { + ...options, + postEvent: this.postEvent, + }) + .then((data) => data.status) + .finally(() => { + this.requestingWriteAccess = false; + }); } /** @@ -203,11 +314,11 @@ export class MiniApp { * Checks if specified method is supported by current component. */ supports: SupportsFunc< - | 'requestWriteAccess' - | 'requestPhoneAccess' - | 'switchInlineQuery' - | 'setHeaderColor' - | 'setBackgroundColor' + | 'requestWriteAccess' + | 'requestPhoneAccess' + | 'switchInlineQuery' + | 'setHeaderColor' + | 'setBackgroundColor' >; /** diff --git a/packages/sdk/src/mini-app/contactParser.ts b/packages/sdk/src/mini-app/contactParser.ts new file mode 100644 index 000000000..16a15b39d --- /dev/null +++ b/packages/sdk/src/mini-app/contactParser.ts @@ -0,0 +1,29 @@ +import { date, json, number, searchParams, string } from '~/parsing/index.js'; + +import type { RequestedContact } from './types.js'; + +export const contactParser = searchParams({ + contact: json({ + userId: { + type: number(), + from: 'user_id', + }, + phoneNumber: { + type: string(), + from: 'phone_number', + }, + firstName: { + type: string(), + from: 'first_name', + }, + lastName: { + type: string(), + from: 'last_name', + }, + }), + authDate: { + type: date(), + from: 'auth_date', + }, + hash: string(), +}); diff --git a/packages/sdk/src/mini-app/types.ts b/packages/sdk/src/mini-app/types.ts index a44da4c2c..ef370e003 100644 --- a/packages/sdk/src/mini-app/types.ts +++ b/packages/sdk/src/mini-app/types.ts @@ -1,6 +1,7 @@ import type { HeaderColorKey, PostEvent } from '~/bridge/index.js'; import type { RGB } from '~/colors/index.js'; import type { StateEvents } from '~/state/index.js'; +import type { CreateRequestIdFunc } from '~/types/index.js'; import type { Version } from '~/version/index.js'; export interface MiniAppProps { @@ -8,6 +9,7 @@ export interface MiniAppProps { backgroundColor: RGB; version: Version; botInline: boolean; + createRequestId: CreateRequestIdFunc; postEvent?: PostEvent; } @@ -23,3 +25,14 @@ export type MiniAppEvents = StateEvents; export type MiniAppEventName = keyof MiniAppEvents; export type MiniAppEventListener = MiniAppEvents[E]; + +export interface RequestedContact { + contact: { + userId: number; + phoneNumber: string; + firstName: string; + lastName: string; + }; + authDate: Date; + hash: string; +} From 96d6c565c39a4db10e005562ccd92e1b1aa08e31 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:42:33 +0300 Subject: [PATCH 10/20] tests(withTimeout): fix withTimeout tests --- packages/sdk/tests/timeout/withTimeout.ts | 48 +++++++---------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/packages/sdk/tests/timeout/withTimeout.ts b/packages/sdk/tests/timeout/withTimeout.ts index e34ab6f60..0aa5291f9 100644 --- a/packages/sdk/tests/timeout/withTimeout.ts +++ b/packages/sdk/tests/timeout/withTimeout.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { afterAll, beforeAll, expect, it, vi } from 'vitest'; import { withTimeout, TimeoutError } from '~/timeout/index.js'; @@ -10,39 +10,19 @@ afterAll(() => { vi.useRealTimers(); }); -describe('wrapped value is function', async () => { - it('should throw an error in case timeout reached', () => { - const wrapped = withTimeout(() => new Promise((res) => { - setTimeout(res, 500); - }), 100); +it('should throw an error in case timeout reached', () => { + const promise = withTimeout(() => new Promise((res) => { + setTimeout(res, 500); + }), 100); - Promise.resolve().then(() => vi.advanceTimersByTime(500)); - expect(wrapped()).rejects.toStrictEqual(new TimeoutError(100)); - }, 1000); + Promise.resolve().then(() => vi.advanceTimersByTime(500)); + expect(promise).rejects.toStrictEqual(new TimeoutError(100)); +}, 1000); - it('should return resolved value by wrapped function', () => { - const wrapped = withTimeout(() => new Promise((res) => { - res('I am fine'); - }), 100); +it('should return resolved value by wrapped function', () => { + const promise = withTimeout(() => new Promise((res) => { + res('I am fine'); + }), 100); - expect(wrapped()).resolves.toBe('I am fine'); - }, 1000); -}); - -describe('wrapped value is promise', () => { - it('should throw an error in case timeout reached', () => { - Promise.resolve().then(() => vi.advanceTimersByTime(500)); - - const wrapped = withTimeout(new Promise((res) => { - setTimeout(res, 500); - }), 100); - - expect(wrapped).rejects.toStrictEqual(new TimeoutError(100)); - }, 1000); - - it('should return resolved value by wrapped function', () => { - const wrapped = withTimeout(Promise.resolve('I am fine'), 100); - - expect(wrapped).resolves.toBe('I am fine'); - }, 1000); -}); \ No newline at end of file + expect(promise).resolves.toBe('I am fine'); +}, 1000); From d90f52152b3dc4a0494707e4be6e628d4b29e354 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:43:46 +0300 Subject: [PATCH 11/20] docs(changeset): Implemented MiniApp.requestContact method, reworked MiniApp.requestWriteAccess and MiniApp.requestPhoneAccess methods. Add invokeCustomMethod function. --- .changeset/neat-fans-matter.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/neat-fans-matter.md diff --git a/.changeset/neat-fans-matter.md b/.changeset/neat-fans-matter.md new file mode 100644 index 000000000..6564f73bb --- /dev/null +++ b/.changeset/neat-fans-matter.md @@ -0,0 +1,5 @@ +--- +"@tma.js/sdk": minor +--- + +Implemented MiniApp.requestContact method, reworked MiniApp.requestWriteAccess and MiniApp.requestPhoneAccess methods. Add invokeCustomMethod function. From 74523d49d2ed0d71d45c98947e218813b18c9a7c Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:45:50 +0300 Subject: [PATCH 12/20] fix(exports): add some more exports from index --- packages/sdk/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 8562ea55f..1d54a77aa 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,6 +6,7 @@ export { export { createPostEvent, isIframe, + invokeCustomMethod, on, off, once, @@ -35,6 +36,7 @@ export { type PhoneRequestedStatus, type PostEvent, type RequestOptions, + type RequestOptionsAdvanced, type SwitchInlineQueryChatType, type WriteAccessRequestedStatus, } from './bridge/index.js'; From db57827531b56fc1c96646c8f3e6888d7091dea9 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 13:46:40 +0300 Subject: [PATCH 13/20] refactor(exports): refactor index exports --- packages/sdk/src/index.ts | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 1d54a77aa..76ca87be6 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -40,10 +40,7 @@ export { type SwitchInlineQueryChatType, type WriteAccessRequestedStatus, } from './bridge/index.js'; -export { - classNames, - mergeClassNames, -} from './classnames/index.js'; +export { classNames, mergeClassNames } from './classnames/index.js'; export { ClosingBehavior, type ClosingBehaviorEventListener, @@ -106,10 +103,7 @@ export { type MiniAppEvents, type MiniAppProps, } from './mini-app/index.js'; -export { - isTMA, - isRecord, -} from './misc/index.js'; +export { isTMA, isRecord } from './misc/index.js'; export { getHash, HashNavigator, @@ -155,15 +149,9 @@ export { type ThemeParamsKey, type ThemeParamsParsed, } from './theme-params/index.js'; -export type { - RequestId, - CreateRequestIdFunc, -} from './types/index.js'; +export type { RequestId, CreateRequestIdFunc } from './types/index.js'; export { Utils } from './utils/index.js'; -export { - compareVersions, - type Version, -} from './version/index.js'; +export { compareVersions, type Version } from './version/index.js'; export { requestViewport, Viewport, @@ -173,7 +161,4 @@ export { type ViewportEventListener, type ViewportEvents, } from './viewport/index.js'; -export { - setTargetOrigin, - setDebug, -} from './globals.js'; +export { setTargetOrigin, setDebug } from './globals.js'; From 6fe72428a39264888b64bfe4a61de6d642cf7761 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 15:20:25 +0300 Subject: [PATCH 14/20] chore(docs): replace Documentation link with Platform and Packages links --- apps/docs/index.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/docs/index.md b/apps/docs/index.md index c46305bf6..d871e2855 100644 --- a/apps/docs/index.md +++ b/apps/docs/index.md @@ -11,8 +11,11 @@ hero: tagline: Simple, flexible, native-like web applications to enhance user experience actions: - theme: brand - text: Documentation + text: Platform link: /platform/about-platform + - theme: alt + text: Packages + link: /packages/tma-js-sdk features: - icon: 💻 From c74d8f106a271e1d4dc4a0783d8035ad0317b10e Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 15:20:59 +0300 Subject: [PATCH 15/20] chore(docs): fix link on index page --- apps/docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/index.md b/apps/docs/index.md index d871e2855..339644d3b 100644 --- a/apps/docs/index.md +++ b/apps/docs/index.md @@ -15,7 +15,7 @@ hero: link: /platform/about-platform - theme: alt text: Packages - link: /packages/tma-js-sdk + link: /packages/typescript/tma-js-sdk/about features: - icon: 💻 From 29c2ec08d9256746544b4424c452a8db89993b13 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 15:45:13 +0300 Subject: [PATCH 16/20] feat(sdk): export some utilities from "timeout" --- packages/sdk/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 76ca87be6..ebf5f5295 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -149,6 +149,7 @@ export { type ThemeParamsKey, type ThemeParamsParsed, } from './theme-params/index.js'; +export { withTimeout, TimeoutError, isTimeoutError } from './timeout/index.js'; export type { RequestId, CreateRequestIdFunc } from './types/index.js'; export { Utils } from './utils/index.js'; export { compareVersions, type Version } from './version/index.js'; From cb3ab5f6396dbdb0cb5af70db532a31c45bdaf69 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 15:45:44 +0300 Subject: [PATCH 17/20] docs(sdk): add "request" and "invokeCustomMethod" functions docs --- .../tma-js-sdk/methods-and-events.md | 142 +++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/apps/docs/packages/typescript/tma-js-sdk/methods-and-events.md b/apps/docs/packages/typescript/tma-js-sdk/methods-and-events.md index 06317c827..bcabe1ed1 100644 --- a/apps/docs/packages/typescript/tma-js-sdk/methods-and-events.md +++ b/apps/docs/packages/typescript/tma-js-sdk/methods-and-events.md @@ -17,7 +17,147 @@ This function automatically finds the correct way to send this event based on th environment features. For greater accuracy, it determines the current Telegram application type and selects the appropriate flow. -## Listening to events +### `request` + +`request` function should be used in case, it is required to call some Telegram Mini Apps method +and receive specified event. For example, developer would like to +call [web_app_request_viewport](../../../platform/apps-communication/methods.md#web-app-request-viewport) +method and catch [viewport_changed](../../../platform/apps-communication/events.md#viewport-changed) +event, to receive actual viewport data. + +```typescript +import { request } from '@tma.js/sdk'; + +const viewport = await request('web_app_request_viewport', 'viewport_changed'); + +console.log(viewport); +// Output: +// { +// is_state_stable: true, +// is_expanded: false, +// height: 320 +// }; +``` + +In case, Telegram Mini Apps accepts parameters, you should pass them as the second argument: + +```typescript +import { request } from '@tma.js/sdk'; + +const viewport = await request( + 'web_app_invoke_custom_method', + { + req_id: 'abc', + method: 'getStorageValues', + params: { keys: ['a'] } + }, + 'custom_method_invoked', +); +``` + +This function allows passing additional options, such as `postEvent`, `timeout` and `capture`. + +#### `postEvent` + +We use `postEvent` option to override the method, which is used to call the Telegram Mini Apps +method. + +```typescript +import { request, createPostEvent } from '@tma.js/sdk'; + +request('web_app_request_viewport', 'viewport_changed', { + postEvent: createPostEvent('6.5'), +}); +``` + +#### `timeout` + +`timeout` option is responsible for assigning the request timeout. In case, timeout was reached, +an error will be thrown. + +```typescript +import { request, isTimeoutError } from '@tma.js/sdk'; + +try { + await request( + 'web_app_invoke_custom_method', + { + req_id: '1', + method: 'deleteStorageValues', + params: { keys: ['a'] }, + }, + 'custom_method_invoked', + { timeout: 5000 }, + ); +} catch (e) { + if (isTimeoutError(e)) { + console.error('Timeout error:', e); + return; + } + console.error('Some different error', e); +} +``` + +#### `capture` + +`capture` property is a function, that allows developer to determine if occurred Telegram Mini Apps +event should be captured and returned from the `request` function: + +```typescript +request( + 'web_app_open_invoice', + { slug: 'jjKSJnm1k23lodd' }, + 'invoice_closed', + { + postEvent: this.postEvent, + capture(data) { + return slug === data.slug; + }, + }, +) +``` + +In this case, `request` function will capture the event only in case, it has the expected slug. + +## Invoking Custom Methods + +Custom methods are methods, which could be used by Telegram Mini Apps +`web_app_invoke_custom_method` method. It only simplifies usage of such methods and reuses the +`request` function. + +Here is the code example without using this function: + +```typescript +import { request } from '@tma.js/sdk'; + +request( + 'web_app_invoke_custom_method', + { + req_id: 'request_id_1', + method: 'deleteStorageValues', + params: { keys: ['a'] }, + }, + 'custom_method_invoked', +) +``` + +And that is how we could rewrite it using the `invokeCustomMethod` function: + +```typescript +import { invokeCustomMethod } from '@tma.js/sdk'; + +invokeCustomMethod( + 'deleteStorageValues', + { keys: ['a'] }, + 'request_id_1', +); +``` + +In contrary to the `request` function, the `invokeCustomMethod` function parses the result and +checks if contains the `error` property. In case it does, the function will throw the according +error. Otherwise, the `result` property will be returned. + +## Listening to Events ### `on` and `off` From 2e60a34bc665e20e2d69f0d60a21f5ffc459b4a2 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 15:46:00 +0300 Subject: [PATCH 18/20] chore(docs): refactor some titles --- .../packages/typescript/tma-js-sdk/methods-and-events.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/docs/packages/typescript/tma-js-sdk/methods-and-events.md b/apps/docs/packages/typescript/tma-js-sdk/methods-and-events.md index bcabe1ed1..13c24640a 100644 --- a/apps/docs/packages/typescript/tma-js-sdk/methods-and-events.md +++ b/apps/docs/packages/typescript/tma-js-sdk/methods-and-events.md @@ -1,9 +1,9 @@ -# Methods and events +# Methods and Events This section of SDK covers the topic related to [apps communication](../../../platform/apps-communication/flow-definition.md). -## Calling methods +## Calling Methods To call the Telegram Mini Apps methods, developer should use `postEvent` function: @@ -216,7 +216,7 @@ subscribe(listener); unsubscribe(listener); ``` -## Checking method support +## Checking Method Support `postEvent` function itself is not checking if specified method supported by current native Telegram application. To do this, developer could use `supports` function which accepts Telegram Mini Apps @@ -276,7 +276,7 @@ import { setDebug } from '@tma.js/sdk'; setDebug(true); ``` -## Target origin +## Target Origin If the package is being used in a browser environment (iframe), packages employs the function `window.parent.postMessage`. This function requires specifying the target origin to ensure From a02d01cc8ecca4ff0e9a67ee0d9db4dd866531b8 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 15:51:00 +0300 Subject: [PATCH 19/20] docs(sdk): add SettingsButton docs --- apps/docs/.vitepress/packages.ts | 4 ++ .../tma-js-sdk/components/settings-button.md | 52 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 apps/docs/packages/typescript/tma-js-sdk/components/settings-button.md diff --git a/apps/docs/.vitepress/packages.ts b/apps/docs/.vitepress/packages.ts index 8eacad5fc..a409e5318 100644 --- a/apps/docs/.vitepress/packages.ts +++ b/apps/docs/.vitepress/packages.ts @@ -106,6 +106,10 @@ export const packagesSidebar = { text: 'QRScanner', link: prefixed('/typescript/tma-js-sdk/components/qr-scanner'), }, + { + text: 'SettingsButton', + link: prefixed('/typescript/tma-js-sdk/components/settings-button'), + }, { text: 'ThemeParams', link: prefixed('/typescript/tma-js-sdk/components/theme-params'), diff --git a/apps/docs/packages/typescript/tma-js-sdk/components/settings-button.md b/apps/docs/packages/typescript/tma-js-sdk/components/settings-button.md new file mode 100644 index 000000000..f669e39d0 --- /dev/null +++ b/apps/docs/packages/typescript/tma-js-sdk/components/settings-button.md @@ -0,0 +1,52 @@ +# `SettingsButton` + +Implements Telegram Mini Apps [Settings Button](../../../../platform/ui/settings-button.md). + +## Initialization + +Component constructor accepts visibility state, Telegram Mini Apps version and optional function +to call Telegram Mini Apps methods. + +```typescript +import { SettingsButton, postEvent } from '@tma.js/sdk'; + +const settingsButton = new SettingsButton(false, '6.3', postEvent); +``` + +## Showing and hiding + +To show and hide the `SettingsButton`, it is required to use `show()` and `hide()` methods. These +methods update the button's `isVisible` property: + +```typescript +settingsButton.show(); +console.log(settingsButton.isVisible); // true + +settingsButton.hide(); +console.log(settingsButton.isVisible); // false +``` + +## Events + +List of events, which could be used in `on` and `off` component instance methods: + +| Event | Listener | Triggered when | +|------------------|----------------------------|--------------------------------| +| click | `() => void` | Settings Button was clicked | +| change | `() => void` | Something in component changed | +| change:isVisible | `(value: boolean) => void` | `isVisible` property changed | + +## Methods support + +List of methods, which could be used in `supports` component instance method: + +- `show` +- `hide` + +```typescript +import { SettingsButton } from '@tma.js/sdk'; + +const settingsButton = new SettingsButton(...); +settingsButton.supports('show'); +settingsButton.supports('hide'); +``` From 8a630115723f9c56fb12359323c4d61425399fa9 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Fri, 15 Dec 2023 15:51:34 +0300 Subject: [PATCH 20/20] chore(docs): fix typos --- .../packages/typescript/tma-js-sdk/components/main-button.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/packages/typescript/tma-js-sdk/components/main-button.md b/apps/docs/packages/typescript/tma-js-sdk/components/main-button.md index 472caf8a4..60ce33c52 100644 --- a/apps/docs/packages/typescript/tma-js-sdk/components/main-button.md +++ b/apps/docs/packages/typescript/tma-js-sdk/components/main-button.md @@ -9,7 +9,7 @@ visibility state, progress visibility state, text, and its color. It also accept function to call Telegram Mini Apps methods. ```typescript -import { BackButton, postEvent } from '@tma.js/sdk'; +import { MainButton, postEvent } from '@tma.js/sdk'; const mainButton = new MainButton({ backgroundColor: '#aaddfe',