From 2e493c270e5d2f3dc6965d7bc878b71cb80bbbd9 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 01:32:41 +0500 Subject: [PATCH 01/16] docs(events,methods): add emoji status related events and methods --- apps/docs/platform/events.md | 22 ++++++++++++++++++++++ apps/docs/platform/methods.md | 19 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/docs/platform/events.md b/apps/docs/platform/events.md index 7dcd137be..b0a22c5f9 100644 --- a/apps/docs/platform/events.md +++ b/apps/docs/platform/events.md @@ -191,6 +191,28 @@ Custom method invocation completed. | result | `unknown` | _Optional_. Method invocation result. | | error | `string` | _Optional_. Method invocation error code. | +### `emoji_status_access_requested` + +Available since: **v8.0** + +Request to set custom emoji status was requested. + +| Field | Type | Description | +|--------|----------|------------------------------------------------------------| +| status | `string` | Request status. Possible values: `allowed` or `cancelled`. | + +### `emoji_status_failed` + +Available since: **v8.0** + +Failed to set custom emoji status. + +### `emoji_status_set` + +Available since: **v8.0** + +Custom emoji status set. + ### `fullscreen_changed` Available since: **v8.0** diff --git a/apps/docs/platform/methods.md b/apps/docs/platform/methods.md index e30d11de4..537713e12 100644 --- a/apps/docs/platform/methods.md +++ b/apps/docs/platform/methods.md @@ -128,7 +128,7 @@ Opens the biometric access settings for bots. Useful when you need to request bi access to users who haven't granted it yet. > [!INFO] -> This method can be called only in response to user interaction with the Mini App interface +> This method can be called only in response to user interaction with the Mini App interface > (e.g. a click inside the Mini App or on the main button) ### `web_app_biometry_request_access` @@ -388,6 +388,12 @@ Requests the current content safe area information from Telegram. As a result, Telegram triggers the [**`content_safe_area_changed`**](events.md#content-safe-area-changed) event. +### `web_app_request_emoji_status_access` + +Available since: **v8.0** + +Shows a native popup requesting permission for the bot to manage user's emoji status. + ### `web_app_request_fullscreen` Available since: **v8.0** @@ -445,6 +451,17 @@ Updates the Mini App bottom bar background color. |-------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------| | color | `string` | The Mini App bottom bar background color in `#RRGGBB` format, or one of the values: `bg_color`, `secondary_bg_color` or `bottom_bar_bg_color` | +### `web_app_set_emoji_status` + +Available since: **v8.0** + +Opens a dialog allowing the user to set the specified custom emoji as their status. + +| Field | Type | Description | +|-----------------|----------|----------------------------------------------------| +| custom_emoji_id | `string` | Custom emoji identifier to set. | +| duration | `number` | _Optional_. The status expiration time in seconds. | + ### `web_app_set_header_color` Available since: **v6.1** From 23995ce3c91baf9727c5843ce02efc2b2a066f8e Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 01:33:34 +0500 Subject: [PATCH 02/16] feat(bridge): add emoji status related events and methods --- packages/bridge/src/events/types/events.ts | 23 ++++++++++++++++++++ packages/bridge/src/methods/supports.test.ts | 4 +++- packages/bridge/src/methods/supports.ts | 2 ++ packages/bridge/src/methods/types/methods.ts | 21 ++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/bridge/src/events/types/events.ts b/packages/bridge/src/events/types/events.ts index 247234081..eba762a67 100644 --- a/packages/bridge/src/events/types/events.ts +++ b/packages/bridge/src/events/types/events.ts @@ -150,6 +150,29 @@ export interface Events { */ error?: string; }; + /** + * Request to set custom emoji status was requested. + * @see https://docs.telegram-mini-apps.com/platform/events#emoji-status-access-requested + * @since v8.0 + */ + emoji_status_access_requested: { + /** + * Request status. + */ + status: 'allowed' | string; + }; + /** + * Failed to set custom emoji status. + * @see https://docs.telegram-mini-apps.com/platform/events#emoji-status-failed + * @since v8.0 + */ + emoji_status_failed: never; + /** + * Custom emoji status set. + * @see https://docs.telegram-mini-apps.com/platform/events#emoji-status-set + * @since v8.0 + */ + emoji_status_set: never; /** * App entered or exited fullscreen mode. * @since v8.0 diff --git a/packages/bridge/src/methods/supports.test.ts b/packages/bridge/src/methods/supports.test.ts index baffc17eb..95ac16e5f 100644 --- a/packages/bridge/src/methods/supports.test.ts +++ b/packages/bridge/src/methods/supports.test.ts @@ -118,7 +118,9 @@ describe.each<[ ]], ['8.0', [ 'web_app_request_fullscreen', - 'web_app_exit_fullscreen' + 'web_app_exit_fullscreen', + 'web_app_set_emoji_status', + 'web_app_request_emoji_status_access', ]], ])('%s', (version, methods) => { const higher = increaseVersion(version, 1); diff --git a/packages/bridge/src/methods/supports.ts b/packages/bridge/src/methods/supports.ts index 7582c62a2..f58b68c60 100644 --- a/packages/bridge/src/methods/supports.ts +++ b/packages/bridge/src/methods/supports.ts @@ -105,6 +105,8 @@ export function supports( case 'web_app_request_content_safe_area': case 'web_app_request_fullscreen': case 'web_app_exit_fullscreen': + case 'web_app_set_emoji_status': + case 'web_app_request_emoji_status_access': return versionLessOrEqual('8.0', paramOrVersion); default: return [ diff --git a/packages/bridge/src/methods/types/methods.ts b/packages/bridge/src/methods/types/methods.ts index 61088f1f0..4e838710e 100644 --- a/packages/bridge/src/methods/types/methods.ts +++ b/packages/bridge/src/methods/types/methods.ts @@ -287,6 +287,12 @@ export interface Methods { * @see https://docs.telegram-mini-apps.com/platform/methods#web-app-request-content-safe-area */ web_app_request_content_safe_area: CreateMethodParams; + /** + * Shows a native popup requesting permission for the bot to manage user's emoji status. + * @since v8.0 + * @see https://docs.telegram-mini-apps.com/platform/methods#web-app-request-emoji-status-access + */ + web_app_request_emoji_status_access: CreateMethodParams; /** * Requests to open the mini app in fullscreen. * @since v8.0 @@ -344,6 +350,21 @@ export interface Methods { */ color: BottomBarColor; }>; + /** + * Opens a dialog allowing the user to set the specified custom emoji as their status. + * @since v8.0 + * @see https://docs.telegram-mini-apps.com/platform/methods#web-app-set-emoji-status + */ + web_app_set_emoji_status: CreateMethodParams<{ + /** + * Custom emoji identifier to set. + */ + custom_emoji_id: string; + /** + * The status expiration time in seconds. + */ + duration?: number; + }>; /** * Updates the Mini App header color. * @since v6.1 From 7c6d9d33e854b74f502bb603e8cfc4c132a0dece Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 01:38:07 +0500 Subject: [PATCH 03/16] docs(changeset): Add methods and events connected with custom emoji set. --- .changeset/hungry-mails-buy.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hungry-mails-buy.md diff --git a/.changeset/hungry-mails-buy.md b/.changeset/hungry-mails-buy.md new file mode 100644 index 000000000..6f60ce023 --- /dev/null +++ b/.changeset/hungry-mails-buy.md @@ -0,0 +1,5 @@ +--- +"@telegram-apps/bridge": minor +--- + +Add methods and events connected with custom emoji set. From 81c19281eac55cf06aa6d76ade745cbbd8e3fc1b Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 01:40:47 +0500 Subject: [PATCH 04/16] chore(changesets): add some packages to ignore --- .changeset/config.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.changeset/config.json b/.changeset/config.json index ffa835aad..513ff766e 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -8,7 +8,10 @@ "baseBranch": "master", "updateInternalDependencies": "patch", "ignore": [ + "docs", "custom-playground", + "vue-template", + "svelte-template", "nextjs-template", "reactjs-template", "solidjs-template" From 1e6ec4d569fb02a6e2efe1e70be031278ff7b86f Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 02:10:13 +0500 Subject: [PATCH 05/16] feat(bridge): create separate type for emoji status access request --- packages/bridge/src/events/types/events.ts | 5 +++-- packages/bridge/src/events/types/misc.ts | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/bridge/src/events/types/events.ts b/packages/bridge/src/events/types/events.ts index eba762a67..b32247866 100644 --- a/packages/bridge/src/events/types/events.ts +++ b/packages/bridge/src/events/types/events.ts @@ -1,6 +1,6 @@ import type { RGB } from '@telegram-apps/types'; -import type { +import { PhoneRequestedStatus, InvoiceStatus, WriteAccessRequestedStatus, @@ -9,6 +9,7 @@ import type { BiometryTokenUpdateStatus, SafeAreaInsets, FullScreenErrorStatus, + EmojiStatusAccessRequestedStatus, } from './misc.js'; /** @@ -159,7 +160,7 @@ export interface Events { /** * Request status. */ - status: 'allowed' | string; + status: EmojiStatusAccessRequestedStatus; }; /** * Failed to set custom emoji status. diff --git a/packages/bridge/src/events/types/misc.ts b/packages/bridge/src/events/types/misc.ts index 2ef5d539f..e5551a27f 100644 --- a/packages/bridge/src/events/types/misc.ts +++ b/packages/bridge/src/events/types/misc.ts @@ -7,6 +7,8 @@ export type InvoiceStatus = export type PhoneRequestedStatus = 'sent' | 'cancelled' | string; +export type EmojiStatusAccessRequestedStatus = 'allowed' | string; + export type WriteAccessRequestedStatus = 'allowed' | string; export type BiometryType = 'finger' | 'face' | string; From e3d5c5e4f0bcdd45686072a6ff594de3c4b51062 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 02:10:34 +0500 Subject: [PATCH 06/16] feat(sdk): implement requestEmojiStatusAccess --- .../src/scopes/utilities/privacy/exports.ts | 6 +++ .../privacy/requestEmojiStatusAccess.ts | 53 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 packages/sdk/src/scopes/utilities/privacy/requestEmojiStatusAccess.ts diff --git a/packages/sdk/src/scopes/utilities/privacy/exports.ts b/packages/sdk/src/scopes/utilities/privacy/exports.ts index c065aaa57..3c68706ca 100644 --- a/packages/sdk/src/scopes/utilities/privacy/exports.ts +++ b/packages/sdk/src/scopes/utilities/privacy/exports.ts @@ -1,3 +1,9 @@ export { requestContact, type RequestedContact } from './requestContact.js'; +export { + requestEmojiStatusAccess, + requestEmojiStatusAccessError, + requestEmojiStatusAccessPromise, + isRequestingEmojiStatusAccess, +} from './requestEmojiStatusAccess.js'; export { requestPhoneAccess, isRequestingPhoneAccess } from './requestPhoneAccess.js'; export { requestWriteAccess, isRequestingWriteAccess } from './requestWriteAccess.js'; \ No newline at end of file diff --git a/packages/sdk/src/scopes/utilities/privacy/requestEmojiStatusAccess.ts b/packages/sdk/src/scopes/utilities/privacy/requestEmojiStatusAccess.ts new file mode 100644 index 000000000..371027f52 --- /dev/null +++ b/packages/sdk/src/scopes/utilities/privacy/requestEmojiStatusAccess.ts @@ -0,0 +1,53 @@ +import { computed, signal } from '@telegram-apps/signals'; +import { + type AsyncOptions, + type CancelablePromise, + type EmojiStatusAccessRequestedStatus, + TypedError, +} from '@telegram-apps/bridge'; + +import { ERR_ALREADY_REQUESTING } from '@/errors.js'; +import { request } from '@/scopes/globals.js'; +import { wrapSafe } from '@/scopes/toolkit/wrapSafe.js'; +import { signalifyAsyncFn } from '@/scopes/signalifyAsyncFn.js'; + +const METHOD = 'web_app_request_emoji_status_access'; + +export const requestEmojiStatusAccessPromise = signal | undefined>(); + +export const requestEmojiStatusAccessError = signal(); + +/** + * Signal indicating if the emoji status set is currently being requested. + */ +export const isRequestingEmojiStatusAccess = computed(() => !!requestEmojiStatusAccessPromise()); + +/** + * Shows a native popup requesting permission for the bot to manage user's emoji status. + * @param options - additional options. + * @since Mini Apps v8.0 + * @throws {TypedError} ERR_ALREADY_REQUESTING + * @throws {TypedError} ERR_UNKNOWN_ENV + * @throws {TypedError} ERR_NOT_INITIALIZED + * @throws {TypedError} ERR_NOT_SUPPORTED + * @example + * if (requestEmojiStatusAccess.isAvailable()) { + * const status = await requestEmojiStatusAccess(); + * } + */ +export const requestEmojiStatusAccess = wrapSafe( + 'requestPhoneAccess', + signalifyAsyncFn( + (options?: AsyncOptions): CancelablePromise => { + return request(METHOD, 'emoji_status_access_requested', options) + .then(r => r.status); + }, + () => new TypedError( + ERR_ALREADY_REQUESTING, + 'Emoji status access request is currently in progress', + ), + requestEmojiStatusAccessPromise, + requestEmojiStatusAccessError, + ), + { isSupported: METHOD }, +); \ No newline at end of file From 5774f3eb4eea1135d0541706f17839002d222d52 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 02:53:50 +0500 Subject: [PATCH 07/16] docs(events): add payload data for emoji_status_failed --- apps/docs/platform/events.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/docs/platform/events.md b/apps/docs/platform/events.md index b0a22c5f9..bb0406eb7 100644 --- a/apps/docs/platform/events.md +++ b/apps/docs/platform/events.md @@ -207,6 +207,10 @@ Available since: **v8.0** Failed to set custom emoji status. +| Field | Type | Description | +|-------|----------|-----------------------------------------------------------------------------------------| +| error | `string` | Emoji set failure reason. Possible values: `SUGGESTED_EMOJI_INVALID` or `USER_DECLINED` | + ### `emoji_status_set` Available since: **v8.0** From f8db7a01a395be0f22291f4c0d9bfc0cf8bdd2b1 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 02:54:07 +0500 Subject: [PATCH 08/16] feat(bridge): add payload data for emoji_status_failed --- packages/bridge/src/events/types/events.ts | 5 ++++- packages/bridge/src/events/types/misc.ts | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/bridge/src/events/types/events.ts b/packages/bridge/src/events/types/events.ts index b32247866..32a3f8c5a 100644 --- a/packages/bridge/src/events/types/events.ts +++ b/packages/bridge/src/events/types/events.ts @@ -10,6 +10,7 @@ import { SafeAreaInsets, FullScreenErrorStatus, EmojiStatusAccessRequestedStatus, + EmojiStatusFailedError, } from './misc.js'; /** @@ -167,7 +168,9 @@ export interface Events { * @see https://docs.telegram-mini-apps.com/platform/events#emoji-status-failed * @since v8.0 */ - emoji_status_failed: never; + emoji_status_failed: { + error: EmojiStatusFailedError; + }; /** * Custom emoji status set. * @see https://docs.telegram-mini-apps.com/platform/events#emoji-status-set diff --git a/packages/bridge/src/events/types/misc.ts b/packages/bridge/src/events/types/misc.ts index e5551a27f..8e74f23a7 100644 --- a/packages/bridge/src/events/types/misc.ts +++ b/packages/bridge/src/events/types/misc.ts @@ -9,6 +9,8 @@ export type PhoneRequestedStatus = 'sent' | 'cancelled' | string; export type EmojiStatusAccessRequestedStatus = 'allowed' | string; +export type EmojiStatusFailedError = 'SUGGESTED_EMOJI_INVALID' | 'USER_DECLINED' | string; + export type WriteAccessRequestedStatus = 'allowed' | string; export type BiometryType = 'finger' | 'face' | string; From 0cc4e70d8f3b2620f1b8d32efb45f78a526ee8ed Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 02:54:54 +0500 Subject: [PATCH 09/16] feat(sdk): enhance signalifyAsyncFn and allow passing custom arguments --- packages/sdk/src/scopes/signalifyAsyncFn.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/scopes/signalifyAsyncFn.ts b/packages/sdk/src/scopes/signalifyAsyncFn.ts index 8f6970f2e..e16162efa 100644 --- a/packages/sdk/src/scopes/signalifyAsyncFn.ts +++ b/packages/sdk/src/scopes/signalifyAsyncFn.ts @@ -1,7 +1,7 @@ import { batch, type Signal } from '@telegram-apps/signals'; -import { type AsyncOptions, CancelablePromise, type TypedError } from '@telegram-apps/bridge'; +import { CancelablePromise, type TypedError } from '@telegram-apps/bridge'; -type AllowedFn = (options?: AsyncOptions) => R | CancelablePromise; +type AllowedFn = (...args: Args) => R | CancelablePromise; /** * Function doing the following: @@ -13,13 +13,13 @@ type AllowedFn = (options?: AsyncOptions) => R | CancelablePromise; * @param error - signal containing the last call error. */ // #__NO_SIDE_EFFECTS__ -export function signalifyAsyncFn, Result>( +export function signalifyAsyncFn, Args extends any[], Result>( fn: Fn, createPendingError: () => TypedError, promise: Signal | undefined>, error: Signal, ): Fn { - return Object.assign((options?: AsyncOptions): CancelablePromise => { + return Object.assign((...args: Args): CancelablePromise => { return CancelablePromise .resolve() .then(async () => { @@ -32,7 +32,7 @@ export function signalifyAsyncFn, Result>( // Start performing the wrapped function. batch(() => { - promise.set(CancelablePromise.resolve(fn(options))); + promise.set(CancelablePromise.resolve(fn(...args))); error.set(undefined); }); let result: [completed: true, result: Result] | [completed: false, err: Error]; From e04fdc2e02759dd39673db35d1770225c3fb9991 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 02:57:30 +0500 Subject: [PATCH 10/16] feat(sdk): implement setEmojiStatus. Move requestEmojiStatusAccess to emoji-status scope --- packages/sdk/src/errors.ts | 1 + packages/sdk/src/index.ts | 2 + .../scopes/utilities/emoji-status/exports.ts | 13 ++++ .../requestEmojiStatusAccess.ts | 10 ++- .../utilities/emoji-status/setEmojiStatus.ts | 77 +++++++++++++++++++ .../src/scopes/utilities/privacy/exports.ts | 6 -- 6 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 packages/sdk/src/scopes/utilities/emoji-status/exports.ts rename packages/sdk/src/scopes/utilities/{privacy => emoji-status}/requestEmojiStatusAccess.ts (87%) create mode 100644 packages/sdk/src/scopes/utilities/emoji-status/setEmojiStatus.ts diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts index dca30f8e3..a8a69a1bc 100644 --- a/packages/sdk/src/errors.ts +++ b/packages/sdk/src/errors.ts @@ -12,3 +12,4 @@ export const ERR_NOT_INITIALIZED = 'ERR_NOT_INITIALIZED'; export const ERR_NOT_SUPPORTED = 'ERR_NOT_SUPPORTED'; export const ERR_NOT_MOUNTED = 'ERR_NOT_MOUNTED'; export const ERR_FULLSCREEN_FAILED = 'ERR_FULLSCREEN_FAILED'; +export const ERR_EMOJI_STATUS_SET_FAILED = 'ERR_EMOJI_STATUS_SET_FAILED'; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index fa7e8e82e..0d1204cc0 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -17,6 +17,7 @@ export * from '@/scopes/components/settings-button/exports.js'; export * from '@/scopes/components/swipe-behavior/exports.js'; export * from '@/scopes/components/theme-params/exports.js'; export * from '@/scopes/components/viewport/exports.js'; +export * from '@/scopes/utilities/emoji-status/exports.js'; export * from '@/scopes/utilities/links/exports.js'; export * from '@/scopes/utilities/privacy/exports.js'; export * from '@/scopes/utilities/uncategorized/exports.js'; @@ -41,6 +42,7 @@ export { ERR_ALREADY_MOUNTING, ERR_ALREADY_REQUESTING, ERR_FULLSCREEN_FAILED, + ERR_EMOJI_STATUS_SET_FAILED, } from '@/errors.js'; export { init, type InitOptions } from '@/init.js'; diff --git a/packages/sdk/src/scopes/utilities/emoji-status/exports.ts b/packages/sdk/src/scopes/utilities/emoji-status/exports.ts new file mode 100644 index 000000000..7e271280f --- /dev/null +++ b/packages/sdk/src/scopes/utilities/emoji-status/exports.ts @@ -0,0 +1,13 @@ +export { + requestEmojiStatusAccess, + requestEmojiStatusAccessError, + requestEmojiStatusAccessPromise, + isRequestingEmojiStatusAccess, +} from './requestEmojiStatusAccess.js'; +export { + setEmojiStatus, + isSettingEmojiStatus, + setEmojiStatusError, + setEmojiStatusPromise, + type SetEmojiStatusOptions, +} from './setEmojiStatus.js'; \ No newline at end of file diff --git a/packages/sdk/src/scopes/utilities/privacy/requestEmojiStatusAccess.ts b/packages/sdk/src/scopes/utilities/emoji-status/requestEmojiStatusAccess.ts similarity index 87% rename from packages/sdk/src/scopes/utilities/privacy/requestEmojiStatusAccess.ts rename to packages/sdk/src/scopes/utilities/emoji-status/requestEmojiStatusAccess.ts index 371027f52..4f82e055b 100644 --- a/packages/sdk/src/scopes/utilities/privacy/requestEmojiStatusAccess.ts +++ b/packages/sdk/src/scopes/utilities/emoji-status/requestEmojiStatusAccess.ts @@ -13,12 +13,18 @@ import { signalifyAsyncFn } from '@/scopes/signalifyAsyncFn.js'; const METHOD = 'web_app_request_emoji_status_access'; +/** + * Signal containing the emoji status access request promise. + */ export const requestEmojiStatusAccessPromise = signal | undefined>(); +/** + * Signal containing the last emoji status access request error. + */ export const requestEmojiStatusAccessError = signal(); /** - * Signal indicating if the emoji status set is currently being requested. + * Signal indicating if the emoji status access is currently being requested. */ export const isRequestingEmojiStatusAccess = computed(() => !!requestEmojiStatusAccessPromise()); @@ -36,7 +42,7 @@ export const isRequestingEmojiStatusAccess = computed(() => !!requestEmojiStatus * } */ export const requestEmojiStatusAccess = wrapSafe( - 'requestPhoneAccess', + 'requestEmojiStatusAccess', signalifyAsyncFn( (options?: AsyncOptions): CancelablePromise => { return request(METHOD, 'emoji_status_access_requested', options) diff --git a/packages/sdk/src/scopes/utilities/emoji-status/setEmojiStatus.ts b/packages/sdk/src/scopes/utilities/emoji-status/setEmojiStatus.ts new file mode 100644 index 000000000..cecf2437b --- /dev/null +++ b/packages/sdk/src/scopes/utilities/emoji-status/setEmojiStatus.ts @@ -0,0 +1,77 @@ +import { computed, signal } from '@telegram-apps/signals'; +import { + type AsyncOptions, + type CancelablePromise, + TypedError, +} from '@telegram-apps/bridge'; + +import { ERR_ALREADY_REQUESTING, ERR_EMOJI_STATUS_SET_FAILED } from '@/errors.js'; +import { request } from '@/scopes/globals.js'; +import { wrapSafe } from '@/scopes/toolkit/wrapSafe.js'; +import { signalifyAsyncFn } from '@/scopes/signalifyAsyncFn.js'; + +export interface SetEmojiStatusOptions extends AsyncOptions { + /** + * The status expiration time in seconds. + */ + duration?: number; +} + +const METHOD = 'web_app_set_emoji_status'; + +/** + * Signal containing the emoji status access request promise. + */ +export const setEmojiStatusPromise = signal | undefined>(); + +/** + * Signal containing the last emoji status access request error. + */ +export const setEmojiStatusError = signal(); + +/** + * Signal indicating if the emoji status set is currently being requested. + */ +export const isSettingEmojiStatus = computed(() => !!setEmojiStatusPromise()); + +/** + * Opens a dialog allowing the user to set the specified custom emoji as their status. + * @returns Promise with boolean value indicating if the status was set. + * @param options - additional options. + * @since Mini Apps v8.0 + * @throws {TypedError} ERR_ALREADY_REQUESTING + * @throws {TypedError} ERR_UNKNOWN_ENV + * @throws {TypedError} ERR_NOT_INITIALIZED + * @throws {TypedError} ERR_NOT_SUPPORTED + * @example + * if (setEmojiStatus.isAvailable()) { + * const statusSet = await setEmojiStatus('5361800828313167608'); + * } + */ +export const setEmojiStatus = wrapSafe( + 'setEmojiStatus', + signalifyAsyncFn( + (customEmojiId: string, options?: SetEmojiStatusOptions): CancelablePromise => { + options ||= {}; + return request(METHOD, ['emoji_status_set', 'emoji_status_failed'], { + ...options, + params: { + custom_emoji_id: customEmojiId, + duration: options.duration, + }, + }) + .then(r => { + if (r && 'error' in r) { + throw new TypedError(ERR_EMOJI_STATUS_SET_FAILED, 'Failed to set emoji status', r.error); + } + }); + }, + () => new TypedError( + ERR_ALREADY_REQUESTING, + 'Emoji status set request is currently in progress', + ), + setEmojiStatusPromise, + setEmojiStatusError, + ), + { isSupported: METHOD }, +); \ No newline at end of file diff --git a/packages/sdk/src/scopes/utilities/privacy/exports.ts b/packages/sdk/src/scopes/utilities/privacy/exports.ts index 3c68706ca..c065aaa57 100644 --- a/packages/sdk/src/scopes/utilities/privacy/exports.ts +++ b/packages/sdk/src/scopes/utilities/privacy/exports.ts @@ -1,9 +1,3 @@ export { requestContact, type RequestedContact } from './requestContact.js'; -export { - requestEmojiStatusAccess, - requestEmojiStatusAccessError, - requestEmojiStatusAccessPromise, - isRequestingEmojiStatusAccess, -} from './requestEmojiStatusAccess.js'; export { requestPhoneAccess, isRequestingPhoneAccess } from './requestPhoneAccess.js'; export { requestWriteAccess, isRequestingWriteAccess } from './requestWriteAccess.js'; \ No newline at end of file From cde989bcbfb46f4be32ab728848f6ddf70c545b5 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 23:02:59 +0500 Subject: [PATCH 11/16] feat(sdk): allow using non-cancelable promises in signalifyAsyncFn --- packages/sdk/src/scopes/signalifyAsyncFn.ts | 63 ++++++++++++++++++--- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/sdk/src/scopes/signalifyAsyncFn.ts b/packages/sdk/src/scopes/signalifyAsyncFn.ts index e16162efa..068793435 100644 --- a/packages/sdk/src/scopes/signalifyAsyncFn.ts +++ b/packages/sdk/src/scopes/signalifyAsyncFn.ts @@ -1,26 +1,69 @@ import { batch, type Signal } from '@telegram-apps/signals'; -import { CancelablePromise, type TypedError } from '@telegram-apps/bridge'; +import { CancelablePromise, If, type TypedError } from '@telegram-apps/bridge'; +import { AnyFn } from '@/types.js'; -type AllowedFn = (...args: Args) => R | CancelablePromise; +type Signalified = (...args: Parameters) => If< + Cancelable, + CancelablePromise>, + PromiseLike> +>; /** * Function doing the following: * 1. Prevents the wrapped function from being called concurrently. * 2. Being called, updates the passed promise and error signals. + * + * As a result, the function returns a new one, returning a cancelable promise. * @param fn - function to wrap. * @param createPendingError - function that creates error in case of concurrent call * @param promise - signal containing the execution promise * @param error - signal containing the last call error. + * @param cancelable - is result cancelable. True by default. */ +export function signalifyAsyncFn( + fn: Fn, + createPendingError: () => TypedError, + promise: Signal>> | undefined>, + error: Signal, + cancelable?: true, +): Signalified; + +/** + * Function doing the following: + * 1. Prevents the wrapped function from being called concurrently. + * 2. Being called, updates the passed promise and error signals. + * + * As a result, the function returns a new one, returning a non-cancelable promise. + * @param fn - function to wrap. + * @param createPendingError - function that creates error in case of concurrent call + * @param promise - signal containing the execution promise + * @param error - signal containing the last call error. + * @param cancelable - is result cancelable. True by default. + */ +export function signalifyAsyncFn( + fn: Fn, + createPendingError: () => TypedError, + promise: Signal>> | undefined>, + error: Signal, + cancelable: false, +): Signalified; + // #__NO_SIDE_EFFECTS__ -export function signalifyAsyncFn, Args extends any[], Result>( +export function signalifyAsyncFn( fn: Fn, createPendingError: () => TypedError, - promise: Signal | undefined>, + promise: + | Signal>> | undefined> + | Signal>> | undefined>, error: Signal, -): Fn { - return Object.assign((...args: Args): CancelablePromise => { - return CancelablePromise + cancelable?: boolean, +): Signalified { + const PromiseConstructor = cancelable === undefined || cancelable + ? CancelablePromise + : Promise; + + return Object.assign((...args: Parameters): PromiseLike> => { + return PromiseConstructor .resolve() .then(async () => { // Check if the operation is currently not in progress. @@ -32,10 +75,12 @@ export function signalifyAsyncFn, Args extend // Start performing the wrapped function. batch(() => { - promise.set(CancelablePromise.resolve(fn(...args))); + promise.set( + (PromiseConstructor as typeof Promise).resolve(fn(...args)) as unknown as any, + ); error.set(undefined); }); - let result: [completed: true, result: Result] | [completed: false, err: Error]; + let result: [completed: true, result: ReturnType] | [completed: false, err: Error]; try { result = [true, await promise()!]; } catch (e) { From 6a88a04c4657e7d86513f818a0d9f833176677ff Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 23:05:07 +0500 Subject: [PATCH 12/16] fix(sdk): set more accurate type of promise in createMountFn --- packages/sdk/src/scopes/createMountFn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/scopes/createMountFn.ts b/packages/sdk/src/scopes/createMountFn.ts index fea1c6d44..0fab7e60d 100644 --- a/packages/sdk/src/scopes/createMountFn.ts +++ b/packages/sdk/src/scopes/createMountFn.ts @@ -23,7 +23,7 @@ export function createMountFn( mount: (options?: AsyncOptions) => R | CancelablePromise, onMounted: (result: R) => void, isMounted: Signal, - promise: Signal | undefined>, + promise: Signal> | undefined>, error: Signal, ): (options?: AsyncOptions) => CancelablePromise { const noConcurrent = signalifyAsyncFn( From f746c5e6674eb0c00777d56345d807bfaa718c1e Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 23:05:30 +0500 Subject: [PATCH 13/16] fix(sdk): make emoji status-related functions non-cancelable --- .../scopes/utilities/emoji-status/exports.ts | 1 - .../emoji-status/requestEmojiStatusAccess.ts | 14 +++----- .../utilities/emoji-status/setEmojiStatus.ts | 35 ++++++------------- 3 files changed, 16 insertions(+), 34 deletions(-) diff --git a/packages/sdk/src/scopes/utilities/emoji-status/exports.ts b/packages/sdk/src/scopes/utilities/emoji-status/exports.ts index 7e271280f..f6e44a75e 100644 --- a/packages/sdk/src/scopes/utilities/emoji-status/exports.ts +++ b/packages/sdk/src/scopes/utilities/emoji-status/exports.ts @@ -9,5 +9,4 @@ export { isSettingEmojiStatus, setEmojiStatusError, setEmojiStatusPromise, - type SetEmojiStatusOptions, } from './setEmojiStatus.js'; \ No newline at end of file diff --git a/packages/sdk/src/scopes/utilities/emoji-status/requestEmojiStatusAccess.ts b/packages/sdk/src/scopes/utilities/emoji-status/requestEmojiStatusAccess.ts index 4f82e055b..dd8857899 100644 --- a/packages/sdk/src/scopes/utilities/emoji-status/requestEmojiStatusAccess.ts +++ b/packages/sdk/src/scopes/utilities/emoji-status/requestEmojiStatusAccess.ts @@ -1,10 +1,5 @@ import { computed, signal } from '@telegram-apps/signals'; -import { - type AsyncOptions, - type CancelablePromise, - type EmojiStatusAccessRequestedStatus, - TypedError, -} from '@telegram-apps/bridge'; +import { type EmojiStatusAccessRequestedStatus, TypedError } from '@telegram-apps/bridge'; import { ERR_ALREADY_REQUESTING } from '@/errors.js'; import { request } from '@/scopes/globals.js'; @@ -16,7 +11,7 @@ const METHOD = 'web_app_request_emoji_status_access'; /** * Signal containing the emoji status access request promise. */ -export const requestEmojiStatusAccessPromise = signal | undefined>(); +export const requestEmojiStatusAccessPromise = signal | undefined>(); /** * Signal containing the last emoji status access request error. @@ -44,8 +39,8 @@ export const isRequestingEmojiStatusAccess = computed(() => !!requestEmojiStatus export const requestEmojiStatusAccess = wrapSafe( 'requestEmojiStatusAccess', signalifyAsyncFn( - (options?: AsyncOptions): CancelablePromise => { - return request(METHOD, 'emoji_status_access_requested', options) + (): Promise => { + return request(METHOD, 'emoji_status_access_requested') .then(r => r.status); }, () => new TypedError( @@ -54,6 +49,7 @@ export const requestEmojiStatusAccess = wrapSafe( ), requestEmojiStatusAccessPromise, requestEmojiStatusAccessError, + false, ), { isSupported: METHOD }, ); \ No newline at end of file diff --git a/packages/sdk/src/scopes/utilities/emoji-status/setEmojiStatus.ts b/packages/sdk/src/scopes/utilities/emoji-status/setEmojiStatus.ts index cecf2437b..552bedc3c 100644 --- a/packages/sdk/src/scopes/utilities/emoji-status/setEmojiStatus.ts +++ b/packages/sdk/src/scopes/utilities/emoji-status/setEmojiStatus.ts @@ -1,28 +1,17 @@ import { computed, signal } from '@telegram-apps/signals'; -import { - type AsyncOptions, - type CancelablePromise, - TypedError, -} from '@telegram-apps/bridge'; +import { TypedError } from '@telegram-apps/bridge'; import { ERR_ALREADY_REQUESTING, ERR_EMOJI_STATUS_SET_FAILED } from '@/errors.js'; import { request } from '@/scopes/globals.js'; import { wrapSafe } from '@/scopes/toolkit/wrapSafe.js'; import { signalifyAsyncFn } from '@/scopes/signalifyAsyncFn.js'; -export interface SetEmojiStatusOptions extends AsyncOptions { - /** - * The status expiration time in seconds. - */ - duration?: number; -} - const METHOD = 'web_app_set_emoji_status'; /** * Signal containing the emoji status access request promise. */ -export const setEmojiStatusPromise = signal | undefined>(); +export const setEmojiStatusPromise = signal | undefined>(); /** * Signal containing the last emoji status access request error. @@ -51,20 +40,17 @@ export const isSettingEmojiStatus = computed(() => !!setEmojiStatusPromise()); export const setEmojiStatus = wrapSafe( 'setEmojiStatus', signalifyAsyncFn( - (customEmojiId: string, options?: SetEmojiStatusOptions): CancelablePromise => { - options ||= {}; - return request(METHOD, ['emoji_status_set', 'emoji_status_failed'], { - ...options, + async (customEmojiId: string, duration?: number): Promise => { + const result = await request(METHOD, ['emoji_status_set', 'emoji_status_failed'], { params: { custom_emoji_id: customEmojiId, - duration: options.duration, + duration, }, - }) - .then(r => { - if (r && 'error' in r) { - throw new TypedError(ERR_EMOJI_STATUS_SET_FAILED, 'Failed to set emoji status', r.error); - } - }); + }); + + if (result && 'error' in result) { + throw new TypedError(ERR_EMOJI_STATUS_SET_FAILED, 'Failed to set emoji status', result.error); + } }, () => new TypedError( ERR_ALREADY_REQUESTING, @@ -72,6 +58,7 @@ export const setEmojiStatus = wrapSafe( ), setEmojiStatusPromise, setEmojiStatusError, + false, ), { isSupported: METHOD }, ); \ No newline at end of file From 8d01ff792f2c57764c275da5296f01a7547bf0d6 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 23:05:44 +0500 Subject: [PATCH 14/16] docs(sdk): add emoji status-related docs --- apps/docs/.vitepress/packages.ts | 1 + .../2-x/utils/emoji-status.md | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 apps/docs/packages/telegram-apps-sdk/2-x/utils/emoji-status.md diff --git a/apps/docs/.vitepress/packages.ts b/apps/docs/.vitepress/packages.ts index 2aa1ca12b..fd78cd614 100644 --- a/apps/docs/.vitepress/packages.ts +++ b/apps/docs/.vitepress/packages.ts @@ -109,6 +109,7 @@ export const packagesLinksGenerator = (prefix: string = '') => { ]), ], 'Utilities': [{ url: 'utils', page: false }, fromEntries([ + scope('emoji-status'), scope('links'), scope('privacy'), scope('uncategorized'), diff --git a/apps/docs/packages/telegram-apps-sdk/2-x/utils/emoji-status.md b/apps/docs/packages/telegram-apps-sdk/2-x/utils/emoji-status.md new file mode 100644 index 000000000..a2c1109e7 --- /dev/null +++ b/apps/docs/packages/telegram-apps-sdk/2-x/utils/emoji-status.md @@ -0,0 +1,56 @@ +# Emoji Status + +## `requestEmojiStatusAccess` + +To request access to user emoji status update, use the `requestEmojiStatusAccess` function: + +::: code-group + +```ts [Using isAvailable] +import { requestEmojiStatusAccess } from '@telegram-apps/sdk'; + +if (requestEmojiStatusAccess.isAvailable()) { + const status = await requestEmojiStatusAccess(); +} +``` + +```ts [Using ifAvailable] +import { requestEmojiStatusAccess } from '@telegram-apps/sdk'; + +const status = await requestEmojiStatusAccess.ifAvailable(); +``` + +::: + +## `setEmojiStatus` + +To set an emoji status on user's behalf, use the `setEmojiStatus` function. + +As the first argument, it accepts a custom emoji id. Optionally, you can pass the second +argument determining for how many seconds the status must be set. + +::: code-group + +```ts [Using isAvailable] +import { setEmojiStatus } from '@telegram-apps/sdk'; + +if (setEmojiStatus.isAvailable()) { + // Set for unlimited period of time. + await setEmojiStatus('5361800828313167608'); + + // Set for 1 day. + await setEmojiStatus('5361800828313167608', 86400); +} +``` + +```ts [Using ifAvailable] +import { setEmojiStatus } from '@telegram-apps/sdk'; + +// Set for unlimited period of time. +await setEmojiStatus.ifAvailable('5361800828313167608'); + +// Set for 1 day. +await setEmojiStatus.ifAvailable('5361800828313167608', 86400); +``` + +::: \ No newline at end of file From 554750e6ae7f7f40104621cd50f64aaf2d271ae4 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 23:06:43 +0500 Subject: [PATCH 15/16] docs(changeset): Add emoji status-related functionality. --- .changeset/hip-birds-whisper.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hip-birds-whisper.md diff --git a/.changeset/hip-birds-whisper.md b/.changeset/hip-birds-whisper.md new file mode 100644 index 000000000..fbf16c393 --- /dev/null +++ b/.changeset/hip-birds-whisper.md @@ -0,0 +1,5 @@ +--- +"@telegram-apps/sdk": minor +--- + +Add emoji status-related functionality. From 8bd6d16c1f936a3c01370a0b43036ba961b59f87 Mon Sep 17 00:00:00 2001 From: Vladislav Kibenko Date: Tue, 3 Dec 2024 23:11:40 +0500 Subject: [PATCH 16/16] chore(docs/events): fix typo in emoji_status_access_requested --- apps/docs/platform/events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/platform/events.md b/apps/docs/platform/events.md index bb0406eb7..dc5b6de8b 100644 --- a/apps/docs/platform/events.md +++ b/apps/docs/platform/events.md @@ -195,7 +195,7 @@ Custom method invocation completed. Available since: **v8.0** -Request to set custom emoji status was requested. +Access to set custom emoji status was requested. | Field | Type | Description | |--------|----------|------------------------------------------------------------|