UNPKG

4nm

Version:

TypeScript reimplementation of Telegram's official library for communicating with Telegram Web Apps.

508 lines (507 loc) 19.2 kB
import { extractWebAppMeta } from './parsing'; import { InitData } from '../InitData'; import { compareVersions } from '../utils'; import { POPUP_CLOSED_EVENT, THEME_CHANGED_EVENT, VIEWPORT_CHANGED_EVENT, } from '../WebView'; import { formatURL } from '../url'; import { ThemeParams } from '../ThemeParams'; import { isColorDark, toRGBExt } from '../colors'; import { toThemeParamsKey } from './utils'; import { BackButton } from '../BackButton'; import { HapticFeedback } from '../HapticFeedback'; import { MainButton } from '../MainButton'; /** * Represents main WebApp's class. * @see https://core.telegram.org/bots/webapps#initializing-web-apps */ export class WebApp { webView; /** * Creates new empty instance of WebApp. */ static empty(webView) { return new WebApp(webView); } _isClosingConfirmationEnabled = false; _isExpanded = false; _isPopupOpened = false; _headerColor = 'bg_color'; _backgroundColor = 'bg_color'; _viewportHeight; _viewportStableHeight; /** * An object for controlling the main button, which is displayed at the * bottom of the Web App in the Telegram interface. */ mainButton; /** * Current Web App platform. */ platform; /** * An object containing the current theme settings used in the Telegram app. */ theme; /** * Current Web App version. */ version; /** * Web application init data. */ initData; constructor(webView) { this.webView = webView; const theme = ThemeParams.empty(); this.backButton = new BackButton(webView, this); this.initData = InitData.empty(); this.haptic = new HapticFeedback(webView, this); this.mainButton = new MainButton(this.webView, { textColor: theme.buttonTextColor, color: theme.buttonColor, }); this.platform = 'unknown'; this.theme = ThemeParams.empty(); this.version = '6.0'; this._viewportStableHeight = 0; this._viewportHeight = 0; } /** * Updates current status of closing confirmation request. * @since WebApp version 6.2+ * @see requireVersion */ setClosingConfirmationEnabled(enabled) { this.requireVersion('6.2'); this._isClosingConfirmationEnabled = enabled; this.webView.postEvent('web_app_setup_closing_behavior', { need_confirmation: enabled, }); } /** * Returns current native application background color. */ get backgroundColor() { return this._backgroundColor === 'bg_color' || this._backgroundColor === 'secondary_bg_color' ? this.theme[toThemeParamsKey(this._backgroundColor)] : this._backgroundColor; } /** * An object for controlling the back button which can be displayed in the * header of the Web App in the Telegram interface. */ backButton; /** * The color scheme currently used in the Telegram app. */ get colorScheme() { const bgColor = this.theme.backgroundColor; return bgColor === undefined ? 'dark' : isColorDark(bgColor) ? 'dark' : 'light'; } /** * A method that closes the Web App. */ close() { this.webView.postEvent('web_app_close'); } /** * A method that disables the confirmation dialog while the user is trying * to close the Web App. * @see setClosingConfirmationEnabled */ disableClosingConfirmation() { this.setClosingConfirmationEnabled(false); } /** * A method that enables a confirmation dialog while the user is trying to * close the Web App. * @see setClosingConfirmationEnabled */ enableClosingConfirmation() { this.setClosingConfirmationEnabled(true); } /** * A method that expands the Web App to the maximum available height. To * find out if the Web App is expanded to the maximum height, refer to the * value of the `isExpanded`. * @see isExpanded */ expand() { this.webView.postEvent('web_app_expand'); } /** * Returns current native application header color. */ get headerColor() { return this.theme[toThemeParamsKey(this._headerColor)]; } /** * An object for controlling haptic feedback. */ haptic; /** * `true`, if the confirmation dialog is enabled while the user is trying to * close the Web App. */ get isClosingConfirmationEnabled() { return this._isClosingConfirmationEnabled; } /** * `true`, if the Web App is expanded to the maximum available height. * `false`, if the Web App occupies part of the screen and can be expanded * to the full height using `expand` method. * @see expand */ get isExpanded() { return this._isExpanded; } /** * Return true in case, passed version is more than or equal to current * WebApp version. * TODO: Should we check if current version and compared version has * incorrect format (symbols)? * @param version - compared version. */ isVersionAtLeast(version) { return compareVersions(this.version, version) >= 0; } /** * Starts initialization process which gets required data from current * web view. * @see loadFromSearchParams */ init() { // Get all required WebApp parameters. const { version, platform, initData, themeParams, } = extractWebAppMeta(this.webView.initParams); this.platform = platform; this.version = version; this.initData = initData; this._viewportHeight = window.innerHeight; // TODO: Should use main button height? this._viewportStableHeight = window.innerHeight; // TODO: Should use main button height? this.theme = themeParams; // TODO: Detect expansion? Should probably always be true on tdesktop. this._isExpanded = false; // Update Main button info. const buttonColor = themeParams.buttonColor; const buttonTextColor = themeParams.buttonTextColor; if (buttonColor !== undefined) { this.mainButton.setColor(buttonColor); } if (buttonTextColor !== undefined) { this.mainButton.setTextColor(buttonTextColor); } // Add event listener which will update popup open status. this.webView.on(POPUP_CLOSED_EVENT, () => this._isPopupOpened = false); // Add event listener which will listen to theme changes. this.webView.on(THEME_CHANGED_EVENT, theme => this.theme = theme); // Add event listener which will listen to viewport changes. this.webView.on(VIEWPORT_CHANGED_EVENT, (height, isExpanded, isStable) => { this._viewportHeight = height; this._isExpanded = isExpanded; if (isStable) { this._viewportStableHeight = height; } }); } /** * A method that opens a link in an external browser. The Web App will not * be closed. * * Note that this method can be called only in response to the user * interaction with the Web App interface (e.g. click inside the Web App * or on the main button) * @see formatURL * @param url - URL to be opened. */ openLink(url) { const formattedURL = formatURL(url); // In case, current version is 6.1+, open link with special native // application event. // TODO: No mention about version in docs. if (this.isVersionAtLeast('6.1')) { this.webView.postEvent('web_app_open_link', { url: formattedURL }); } // Otherwise, do it in legacy way. else { window.open(formattedURL, '_blank'); } } /** * A method that opens a telegram link inside Telegram app. The Web App * will be closed. * @see formatURL * @param url - URL to be opened. * @throws {Error} URL has not allowed hostname. * @since WebApp version 6.1+ */ openTelegramLink(url) { const { hostname, pathname, search } = new URL(formatURL(url)); // We allow opening links with the only 1 hostname. if (hostname !== 't.me') { throw new Error(`URL has not allowed hostname: ${hostname}. Only "t.me" is allowed`); } // In case, current version is 6.1+ or we are currently in iframe, open // link with special native application event. // TODO: Is it correct that calling of this method is allowed in case // it is iframe or v6.1+? Code was taken from source, but no mention in // docs. if (this.webView.isIframe || this.isVersionAtLeast('6.1')) { this.webView.postEvent('web_app_open_tg_link', { url: pathname + search, }); } // Otherwise, do it in legacy way. else { window.location.href = url; } } /** * TODO: Check docs. * FIXME: Implement * A method that opens an invoice using the link url. The Web App will * receive the event invoiceClosed when the invoice is closed. If an optional * callback parameter was passed, the callback function will be called and * the invoice status will be passed as the first argument. * @since Bot API 6.1+ * @param url */ openInvoice(url) { throw new Error('not implemented'); } /** * A method that informs the Telegram app that the Web App is ready to be * displayed. * * It is recommended to call this method as early as possible, as soon as * all essential interface elements are loaded. Once this method is called, * the loading placeholder is hidden and the Web App is shown. * * If the method is not called, the placeholder will be hidden only when * the page is fully loaded. */ ready() { this.webView.postEvent('web_app_ready'); } /** * Checks if current version satisfies minimum (passed) version. * @param version - version number. * @throws {Error} Version of WebApp does not support this method. */ requireVersion(version) { if (!this.isVersionAtLeast(version)) { throw new Error(`Version "${version}" of WebApp does not support this method.`); } } /** * A method used to send data to the bot. When this method is called, a * service message is sent to the bot containing the data of the * length up to 4096 bytes, and the Web App is closed. See the field * `web_app_data` in the class Message. * * This method is only available for Web Apps launched via a Keyboard button. * * @param data - data to send to bot. * @throws {Error} data has incorrect size. */ sendData(data) { // Firstly, compute passed text size in bytes. const size = new Blob([data]).size; if (size === 0 || size > 4096) { throw new Error(`Passed data has incorrect size: ${size}`); } this.webView.postEvent('web_app_data_send', { data: data }); } /** * FIXME: Implement * A method that shows a native popup described by the params argument. * Promise will be resolved when popup is closed. Resolved value will * have an identifier of pressed button in case, it some of them was clicked. * * In case, user clicked outside of popup, or top right popup close button * was clicked, `null` will be returned. * * TODO: Currently, application crashes in case, some parameters are * incorrect. That's why we should check everything. * * @param params - popup parameters. * @since WebApp version 6.2+ * @see requireVersion * @throws {Error} Popup is already opened. */ showPopup(params) { this.requireVersion('6.2'); // Don't allow opening several popups. if (this._isPopupOpened) { throw new Error('Popup is already opened.'); } // Format all required parameters. const message = params.message.trim(); const title = (params.title || '').trim(); const buttons = params.buttons || []; // Check title. if (title.length > 64) { throw new Error(`Title has incorrect size: ${title.length}`); } // Check message. if (message.length === 0 || message.length > 256) { throw new Error(`Message has incorrect size: ${message.length}`); } // Check buttons. if (buttons.length > 3) { throw new Error(`Buttons have incorrect size: ${buttons.length}`); } // Append button in case, there are no buttons passed. if (buttons.length === 0) { buttons.push({ type: 'close' }); } else { // Otherwise, check all the buttons. buttons.forEach(b => { const { id = '' } = b; // Check button ID. if (id.length > 64) { throw new Error(`Button ID has incorrect size: ${id}`); } switch (b.type) { case undefined: case 'default': case 'destructive': if (b.text.length > 64) { const type = b.type || 'default'; throw new Error(`Button text with type "${type}" has incorrect size: ${b.text.length}`); } break; } b.id = id; }); } // Update popup opened status. this._isPopupOpened = true; return new Promise(res => { const listener = (buttonId) => { // Remove event listener. this.webView.off(POPUP_CLOSED_EVENT, listener); // Resolve promise. res(buttonId === undefined ? null : buttonId); }; this.webView.on(POPUP_CLOSED_EVENT, listener); this.webView.postEvent('web_app_open_popup', { title, message, buttons }); }); } /** * A method that shows message in a simple alert with a 'Close' button. * Promise will be resolved when popup is closed. * * @param message - message to display. * @since WebApp version 6.2+ * @see showPopup */ async showAlert(message) { await this.showPopup({ message, buttons: [{ type: 'close' }] }); } /** * A method that shows message in a simple confirmation window with 'OK' * and 'Cancel' buttons. Promise will be resolved when popup is closed. * Resolved value will be `true` in case, user pressed 'OK` button. The * result will be `false` otherwise. * * @param message - message to display. * @since WebApp version 6.2+ * @see showPopup */ async showConfirm(message) { return this .showPopup({ message, buttons: [{ type: 'ok', id: 'ok' }, { type: 'cancel' }], }) .then(id => id === 'ok'); } /** * Updates current application background color. * * @param color - settable color key or color description in known RGB * format. * @since WebApp version 6.1+ * @see requireVersion * @see toRGBExt */ setBackgroundColor(color) { this.requireVersion('6.1'); // In case, passed color has some RGB format, we should convert it // to #RGB. if (color !== 'bg_color' && color !== 'secondary_bg_color') { // Convert passed value to expected #RRGGBB format. color = toRGBExt(color); } // Don't do anything in case, color is the same. if (this._backgroundColor === color) { return; } // Override current background color key. this._backgroundColor = color; // Notify native application about updating current background color. this.webView.postEvent('web_app_set_background_color', { color }); } /** * Updates current application header color. * @param color - settable color key. * @see requireVersion * @since WebApp version 6.1+ */ setHeaderColor(color) { this.requireVersion('6.1'); // Don't do anything in case, color is the same. if (this._headerColor === color) { return; } // Override current header color key. this._headerColor = color; // Notify native application about updating current header color. this.webView.postEvent('web_app_set_header_color', { color_key: color }); } /** * The current height of the visible area of the Web App. * * The application can display just the top part of the Web App, with its * lower part remaining outside the screen area. From this position, the * user can "pull" the Web App to its maximum height, while the bot can do * the same by calling `expand` method. As the position of the Web App * changes, the current height value of the visible area will be updated in * real time. * * Please note that the refresh rate of this value is not sufficient to * smoothly follow the lower border of the window. It should not be used * to pin interface elements to the bottom of the visible area. It's more * appropriate to use the value of the viewportStableHeight field for * this purpose. * * @see expand * @see viewportStableHeight */ get viewportHeight() { return this._viewportHeight; } /** * The height of the visible area of the Web App in its last stable state. * * The application can display just the top part of the Web App, with its * lower part remaining outside the screen area. From this position, * the user can "pull" the Web App to its maximum height, while the bot can * do the same by calling `expand` method. * * Unlike the value of `viewportHeight`, the value of `viewportStableHeight` * does not change as the position of the Web App changes with user * gestures or during animations. The value of `viewportStableHeight` * will be updated after all gestures and animations are completed and * the Web App reaches its final size. * * @see expand * @see viewportHeight */ get viewportStableHeight() { return this._viewportStableHeight; } }