diff --git a/.changeset/curly-cats-poke.md b/.changeset/curly-cats-poke.md new file mode 100644 index 000000000..6ff4cfa6f --- /dev/null +++ b/.changeset/curly-cats-poke.md @@ -0,0 +1,5 @@ +--- +"@telegram-apps/sdk": minor +--- + +Implement add to home screen-related functionality. diff --git a/.changeset/fair-timers-arrive.md b/.changeset/fair-timers-arrive.md new file mode 100644 index 000000000..1c865ab7f --- /dev/null +++ b/.changeset/fair-timers-arrive.md @@ -0,0 +1,5 @@ +--- +"@telegram-apps/toolkit": minor +--- + +Set name for the TypedError class. diff --git a/.changeset/real-pigs-deny.md b/.changeset/real-pigs-deny.md new file mode 100644 index 000000000..d1cadda8d --- /dev/null +++ b/.changeset/real-pigs-deny.md @@ -0,0 +1,5 @@ +--- +"@telegram-apps/bridge": minor +--- + +Implement `home_screen_added`, `home_screen_checked` and `home_screen_failed` events. Implement `web_app_add_to_home_screen` and `web_app_check_home_screen` methods. diff --git a/apps/docs/.vitepress/packages.ts b/apps/docs/.vitepress/packages.ts index fd78cd614..6eec390c1 100644 --- a/apps/docs/.vitepress/packages.ts +++ b/apps/docs/.vitepress/packages.ts @@ -110,6 +110,7 @@ export const packagesLinksGenerator = (prefix: string = '') => { ], 'Utilities': [{ url: 'utils', page: false }, fromEntries([ scope('emoji-status'), + scope('home-screen'), scope('links'), scope('privacy'), scope('uncategorized'), diff --git a/apps/docs/packages/telegram-apps-sdk/2-x/utils/home-screen.md b/apps/docs/packages/telegram-apps-sdk/2-x/utils/home-screen.md new file mode 100644 index 000000000..92bd1c3e8 --- /dev/null +++ b/apps/docs/packages/telegram-apps-sdk/2-x/utils/home-screen.md @@ -0,0 +1,68 @@ +# Home Screen + +## `addToHomeScreen` + +To prompt the user to add the Mini App to the home screen, use the `addToHomeScreen` function. + +::: code-group + +```ts [Using isAvailable] +import { addToHomeScreen } from '@telegram-apps/sdk'; + +if (addToHomeScreen.isAvailable()) { + addToHomeScreen(); +} +``` + +```ts [Using ifAvailable] +import { addToHomeScreen } from '@telegram-apps/sdk'; + +addToHomeScreen.ifAvailable(); +``` + +::: + +To track whether the current Mini App is added to the device's home screen, use +the `onAddedToHomeScreen` and `offAddedToHomeScreen` functions: + +```ts +import { + onAddedToHomeScreen, + onAddToHomeScreenFailed, + offAddedToHomeScreen, + offAddToHomeScreenFailed, +} from '@telegram-apps/sdk'; + +function onAdded() { + console.log('Added'); +} + +onAddedToHomeScreen(onAdded); +offAddedToHomeScreen(onAdded); + +function onFailed() { + console.log('User declined the request'); +} + +onAddToHomeScreenFailed(onFailed); +offAddToHomeScreenFailed(onFailed); +``` + +> [!NOTE] +> If the device cannot determine the installation status, the corresponding event may not be +> received even if the icon has been added. + +## `checkHomeScreenStatus` + +The `checkHomeScreenStatus` function checks if the user has already added the Mini App to the +device's home screen. + +```ts +import { checkHomeScreenStatus } from '@telegram-apps/sdk'; + +if (checkHomeScreenStatus.isAvailable()) { + checkHomeScreenStatus().then(status => { + console.log(status); + }); +} +``` diff --git a/apps/docs/platform/events.md b/apps/docs/platform/events.md index 617c616f0..6ac0f2640 100644 --- a/apps/docs/platform/events.md +++ b/apps/docs/platform/events.md @@ -237,6 +237,35 @@ Occurs whenever the mini app enters or exits the fullscreen mode. |-------|----------|---------------------------------------------------------------------------------------| | error | `string` | Fullscreen mode status error. Possible values: `UNSUPPORTED` or `ALREADY_FULLSCREEN`. | +### `home_screen_added` + +Available since: **v8.0** + +The mini application was added to the device's home screen. + +### `home_screen_checked` + +Available since: **v8.0** + +The status of the mini application being added to the home screen has been checked. + +| Field | Type | Description | +|--------|----------|------------------------------------------------------------------------------------------------------------------------------------| +| status | `string` | The status of the mini application being added to the home screen. Possible values: `unsupported`, `unknown`, `added` and `missed` | + +- `unsupported` – the feature is not supported, and it is not possible to add the icon to the home + screen, +- `unknown` – the feature is supported, and the icon can be added, but it is not possible to + determine if the icon has already been added, +- `added` – the icon has already been added to the home screen, +- `missed` – the icon has not been added to the home screen. + +### `home_screen_failed` + +Available since: **v8.0** + +User declined the request to add the current mini application to the device's home screen. + ### `invoice_closed` An invoice was closed. diff --git a/apps/docs/platform/methods.md b/apps/docs/platform/methods.md index 537713e12..6b6b9df65 100644 --- a/apps/docs/platform/methods.md +++ b/apps/docs/platform/methods.md @@ -114,6 +114,14 @@ event. Notifies parent iframe about the current iframe is going to reload. +### `web_app_add_to_home_screen` + +Available since: **v8.0** + +Prompts the user to add the Mini App to the home screen. Note that if the device cannot +determine the installation status, the event may not be received even if the icon has +been added. + ### `web_app_biometry_get_info` Available since: **v7.2** @@ -162,6 +170,13 @@ string. |-------|----------|-------------------------------------------------| | token | `string` | Token to store. Has max length of 1024 symbols. | +### `web_app_check_home_screen` + +Available since: **v8.0** + +Sends a request to the native Telegram application to check if the current mini +application is added to the device's home screen. + ### `web_app_close` Closes Mini App. diff --git a/packages/bridge/src/events/types/events.ts b/packages/bridge/src/events/types/events.ts index b647ae2e9..d3a9270fe 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 { +import type { PhoneRequestedStatus, InvoiceStatus, WriteAccessRequestedStatus, @@ -11,6 +11,7 @@ import { FullScreenErrorStatus, EmojiStatusAccessRequestedStatus, EmojiStatusFailedError, + HomeScreenStatus, } from './misc.js'; /** @@ -199,6 +200,37 @@ export interface Events { */ error: FullScreenErrorStatus; }; + /** + * The mini application was added to the device's home screen. + * @since v8.0 + * @see https://docs.telegram-mini-apps.com/platform/events#home_screen_added + */ + home_screen_added: never; + /** + * The status of the mini application being added to the home screen has been checked. + * @since v8.0 + * @see https://docs.telegram-mini-apps.com/platform/events#home_screen_checked + */ + home_screen_checked: { + /** + * The status of the mini application being added to the home screen. + * + * Possible values: + * - `unsupported` – the feature is not supported, and it is not possible to add the icon to the home + * screen, + * - `unknown` – the feature is supported, and the icon can be added, but it is not possible to + * determine if the icon has already been added, + * - `added` – the icon has already been added to the home screen, + * - `missed` – the icon has not been added to the home screen. + */ + status?: HomeScreenStatus; + }; + /** + * User declined the request to add the current mini application to the device's home screen. + * @since v8.0 + * @see https://docs.telegram-mini-apps.com/platform/events#home_screen_failed + */ + home_screen_failed: never; /** * An invoice was closed. * @see https://docs.telegram-mini-apps.com/platform/events#invoice-closed diff --git a/packages/bridge/src/events/types/misc.ts b/packages/bridge/src/events/types/misc.ts index 8e74f23a7..1c2b3d0a7 100644 --- a/packages/bridge/src/events/types/misc.ts +++ b/packages/bridge/src/events/types/misc.ts @@ -33,4 +33,6 @@ export interface SafeAreaInsets { bottom: number; left: number; right: number; -} \ No newline at end of file +} + +export type HomeScreenStatus = 'unsupported' | 'unknown' | 'added' | 'missed' | string; \ No newline at end of file diff --git a/packages/bridge/src/methods/supports.test.ts b/packages/bridge/src/methods/supports.test.ts index 95ac16e5f..b75319e8a 100644 --- a/packages/bridge/src/methods/supports.test.ts +++ b/packages/bridge/src/methods/supports.test.ts @@ -121,6 +121,8 @@ describe.each<[ 'web_app_exit_fullscreen', 'web_app_set_emoji_status', 'web_app_request_emoji_status_access', + 'web_app_add_to_home_screen', + 'web_app_check_home_screen', ]], ])('%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 f58b68c60..92628e445 100644 --- a/packages/bridge/src/methods/supports.ts +++ b/packages/bridge/src/methods/supports.ts @@ -106,6 +106,8 @@ export function supports( case 'web_app_request_fullscreen': case 'web_app_exit_fullscreen': case 'web_app_set_emoji_status': + case 'web_app_add_to_home_screen': + case 'web_app_check_home_screen': case 'web_app_request_emoji_status_access': return versionLessOrEqual('8.0', paramOrVersion); default: diff --git a/packages/bridge/src/methods/types/methods.ts b/packages/bridge/src/methods/types/methods.ts index 4e838710e..3b48ec7ee 100644 --- a/packages/bridge/src/methods/types/methods.ts +++ b/packages/bridge/src/methods/types/methods.ts @@ -68,6 +68,14 @@ export interface Methods { * @see https://docs.telegram-mini-apps.com/platform/methods#iframe-will-reload */ iframe_will_reload: CreateMethodParams; + /** + * Prompts the user to add the Mini App to the home screen. Note that if the device cannot + * determine the installation status, the event may not be received even if the icon has + * been added. + * @since v8.0 + * @see https://docs.telegram-mini-apps.com/platform/methods#web-app-add-to-home-screen + */ + web_app_add_to_home_screen: CreateMethodParams; /** * Emitted by bot mini apps to ask the client to initialize the biometric authentication manager * object for the current bot, emitting a `biometry_info_received` event on completion. @@ -148,6 +156,13 @@ export interface Methods { */ token: string; }>; + /** + * Sends a request to the native Telegram application to check if the current mini + * application is added to the device's home screen. + * @since v8.0 + * @see https://docs.telegram-mini-apps.com/platform/methods#web-app-check-home-screen + */ + web_app_check_home_screen: CreateMethodParams; /** * Closes Mini App. * @see https://docs.telegram-mini-apps.com/platform/methods#web-app-close diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 0d1204cc0..6c1755d6c 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -18,6 +18,7 @@ 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/home-screen/exports.js'; export * from '@/scopes/utilities/links/exports.js'; export * from '@/scopes/utilities/privacy/exports.js'; export * from '@/scopes/utilities/uncategorized/exports.js'; diff --git a/packages/sdk/src/scopes/utilities/home-screen/exports.ts b/packages/sdk/src/scopes/utilities/home-screen/exports.ts new file mode 100644 index 000000000..43f2ed6d7 --- /dev/null +++ b/packages/sdk/src/scopes/utilities/home-screen/exports.ts @@ -0,0 +1,11 @@ +export { + checkHomeScreenStatusError, + checkHomeScreenStatusPromise, + isCheckingHomeScreenStatus, + checkHomeScreenStatus, + offAddedToHomeScreen, + onAddedToHomeScreen, + addToHomeScreen, + offAddToHomeScreenFailed, + onAddToHomeScreenFailed, +} from './home-screen.js'; \ No newline at end of file diff --git a/packages/sdk/src/scopes/utilities/home-screen/home-screen.test.ts b/packages/sdk/src/scopes/utilities/home-screen/home-screen.test.ts new file mode 100644 index 000000000..ab6a9c6e4 --- /dev/null +++ b/packages/sdk/src/scopes/utilities/home-screen/home-screen.test.ts @@ -0,0 +1,33 @@ +import { beforeEach, describe, vi } from 'vitest'; + +import { testSafety } from '@test-utils/predefined/testSafety.js'; +import { resetPackageState } from '@test-utils/reset/reset.js'; +import { mockPostEvent } from '@test-utils/mockPostEvent.js'; + +import { + addToHomeScreen, + onAddedToHomeScreen, + offAddedToHomeScreen, + checkHomeScreenStatus, +} from './home-screen.js'; + +beforeEach(() => { + resetPackageState(); + vi.restoreAllMocks(); + mockPostEvent(); +}); + +describe('safety', () => { + testSafety(addToHomeScreen, 'addToHomeScreen', { minVersion: '8.0' }); +}); + +describe.each([ + ['addToHomeScreen', addToHomeScreen], + ['onAddedToHomeScreen', onAddedToHomeScreen], + ['offAddedToHomeScreen', offAddedToHomeScreen], + ['checkHomeScreenStatus', checkHomeScreenStatus], +] as const)('%s', (name, fn) => { + testSafety(fn, name, { + minVersion: '8.0', + }); +}); \ No newline at end of file diff --git a/packages/sdk/src/scopes/utilities/home-screen/home-screen.ts b/packages/sdk/src/scopes/utilities/home-screen/home-screen.ts new file mode 100644 index 000000000..48f88c26a --- /dev/null +++ b/packages/sdk/src/scopes/utilities/home-screen/home-screen.ts @@ -0,0 +1,181 @@ +import { + on, + type EventListener, + off, + type CancelablePromise, + type HomeScreenStatus, + type AsyncOptions, + TypedError, +} from '@telegram-apps/bridge'; + +import { postEvent, request } from '@/scopes/globals.js'; +import { wrapSafe } from '@/scopes/toolkit/wrapSafe.js'; +import { computed, signal } from '@telegram-apps/signals'; +import { signalifyAsyncFn } from '@/scopes/signalifyAsyncFn.js'; +import { ERR_ALREADY_REQUESTING } from '@/errors.js'; + +const METHOD = 'web_app_add_to_home_screen'; + +const wrapOptions = { isSupported: METHOD } as const; + +/** + * Signal containing the home screen status check request promise. + */ +export const checkHomeScreenStatusPromise = signal | undefined>(); + +/** + * Signal containing the home screen status check request error. + */ +export const checkHomeScreenStatusError = signal(); + +/** + * Signal indicating if the home screen status check is currently being requested. + */ +export const isCheckingHomeScreenStatus = computed(() => !!checkHomeScreenStatusPromise()); + +/** + * Prompts the user to add the Mini App to the home screen. + * @since Mini Apps v8.0 + * @throws {TypedError} ERR_UNKNOWN_ENV + * @throws {TypedError} ERR_NOT_INITIALIZED + * @throws {TypedError} ERR_NOT_SUPPORTED + * @example Using `isAvailable` + * if (addToHomeScreen.isAvailable()) { + * addToHomeScreen(); + * } + * @example Using `ifAvailable` + * addToHomeScreen.ifAvailable() + */ +export const addToHomeScreen = wrapSafe( + 'addToHomeScreen', + () => { + postEvent(METHOD); + }, + wrapOptions, +); + +/** + * Sends a request to the native Telegram application to check if the current mini + * application is added to the device's home screen. + * @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 (checkHomeScreenStatus.isAvailable()) { + * const status = await checkHomeScreenStatus(); + * } + */ +export const checkHomeScreenStatus = wrapSafe( + 'checkHomeScreenStatus', + signalifyAsyncFn( + (options?: AsyncOptions): CancelablePromise => { + return request(METHOD, 'home_screen_checked', options) + .then(r => r.status || 'unknown'); + }, + () => new TypedError( + ERR_ALREADY_REQUESTING, + 'Check home screen status request is currently in progress', + ), + checkHomeScreenStatusPromise, + checkHomeScreenStatusError, + ), + wrapOptions, +); + +/** + * Adds the event listener that being called whenever the user adds the current mini app to the + * device's home screen. + * + * Note that if the device cannot determine the installation status, a corresponding event may + * not be received even if the icon has been added. + * @since Mini Apps v8.0 + * @throws {TypedError} ERR_UNKNOWN_ENV + * @throws {TypedError} ERR_NOT_INITIALIZED + * @throws {TypedError} ERR_NOT_SUPPORTED + * @example + * if (onAddedToHomeScreen.isAvailable()) { + * const off = onAddedToHomeScreen(() => { + * console.log('Added'); + * off(); + * }); + * } + */ +export const onAddedToHomeScreen = wrapSafe( + 'onAddedToHomeScreen', + (listener: EventListener<'home_screen_added'>, once?: boolean) => { + return on('home_screen_added', listener, once); + }, + wrapOptions, +); + +/** + * Adds the event listener that being called whenever the user declines the request to add the + * current mini app to the device's home screen. + * @since Mini Apps v8.0 + * @throws {TypedError} ERR_UNKNOWN_ENV + * @throws {TypedError} ERR_NOT_INITIALIZED + * @throws {TypedError} ERR_NOT_SUPPORTED + * @example + * if (onAddToHomeScreenFailed.isAvailable()) { + * const off = onAddToHomeScreenFailed(() => { + * console.log('Failed to add to home screen'); + * off(); + * }); + * } + */ +export const onAddToHomeScreenFailed = wrapSafe( + 'onAddToHomeScreenFailed', + (listener: EventListener<'home_screen_failed'>, once?: boolean) => { + return on('home_screen_failed', listener, once); + }, + wrapOptions, +); + +/** + * Removes add to home screen event listener. + * @since Mini Apps v8.0 + * @throws {TypedError} ERR_UNKNOWN_ENV + * @throws {TypedError} ERR_NOT_INITIALIZED + * @throws {TypedError} ERR_NOT_SUPPORTED + * @example + * if (onAddedToHomeScreen.isAvailable()) { + * const handler = () => { + * console.log('Added'); + * offAddedToHomeScreen(handler); + * }; + * onAddedToHomeScreen(handler); + * } + */ +export const offAddedToHomeScreen = wrapSafe( + 'offAddedToHomeScreen', + (listener: EventListener<'home_screen_added'>) => { + off('home_screen_added', listener); + }, + wrapOptions, +); + +/** + * Removes add to home screen failed event listener. + * @since Mini Apps v8.0 + * @throws {TypedError} ERR_UNKNOWN_ENV + * @throws {TypedError} ERR_NOT_INITIALIZED + * @throws {TypedError} ERR_NOT_SUPPORTED + * @example + * if (offAddToHomeScreenFailed.isAvailable()) { + * const handler = () => { + * console.log('Failed to add'); + * offAddToHomeScreenFailed(handler); + * }; + * onAddToHomeScreenFailed(handler); + * } + */ +export const offAddToHomeScreenFailed = wrapSafe( + 'offAddToHomeScreenFailed', + (listener: EventListener<'home_screen_failed'>) => { + off('home_screen_failed', listener); + }, + wrapOptions, +); diff --git a/packages/sdk/test-utils/reset/reset.ts b/packages/sdk/test-utils/reset/reset.ts index 02645c19f..57b1e6314 100644 --- a/packages/sdk/test-utils/reset/reset.ts +++ b/packages/sdk/test-utils/reset/reset.ts @@ -17,6 +17,7 @@ import { resetSwipeBehavior } from '@test-utils/reset/resetSwipeBehavior.js'; import { resetThemeParams } from '@test-utils/reset/resetThemeParams.js'; import { resetViewport } from '@test-utils/reset/resetViewport.js'; import { resetPrivacy } from '@test-utils/reset/resetPrivacy.js'; +import { resetHomeScreen } from '@test-utils/reset/resetHomeScreen.js'; export function resetSignal(s: Signal | Computed) { s.unsubAll(); @@ -41,6 +42,7 @@ export function resetPackageState() { resetSwipeBehavior, resetThemeParams, resetViewport, + resetHomeScreen, ].forEach(reset => reset()); [$postEvent, $version, $createRequestId].forEach(resetSignal); } diff --git a/packages/sdk/test-utils/reset/resetHomeScreen.ts b/packages/sdk/test-utils/reset/resetHomeScreen.ts new file mode 100644 index 000000000..a9faaaf4b --- /dev/null +++ b/packages/sdk/test-utils/reset/resetHomeScreen.ts @@ -0,0 +1,14 @@ +import { + isCheckingHomeScreenStatus, + checkHomeScreenStatusError, + checkHomeScreenStatusPromise, +} from '@/scopes/utilities/home-screen/home-screen.js'; +import { resetSignal } from '@test-utils/reset/reset.js'; + +export function resetHomeScreen() { + [ + isCheckingHomeScreenStatus, + checkHomeScreenStatusError, + checkHomeScreenStatusPromise, + ].forEach(resetSignal); +} \ No newline at end of file diff --git a/packages/toolkit/src/errors/TypedError.ts b/packages/toolkit/src/errors/TypedError.ts index 81aca9e89..14f43f7d8 100644 --- a/packages/toolkit/src/errors/TypedError.ts +++ b/packages/toolkit/src/errors/TypedError.ts @@ -13,6 +13,7 @@ export class TypedError extends Error { cause: typeof messageOrOptions === 'object' ? messageOrOptions.cause : cause, }, ); + this.name = 'TypedError'; Object.setPrototypeOf(this, TypedError.prototype); } }