├── LICENSE ├── README.md └── telegram-web-app.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Telegram Messenger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript SDK for Telegram Mini Apps 2 | 3 | With **[Mini Apps](https://core.telegram.org/bots/webapps)** developers can use *JavaScript* to create **infinitely flexible interfaces** that can be launched right inside Telegram — and can completely replace **any website**. 4 | 5 | Like bots, **Mini Apps** support [seamless authorization](https://telegram.org/blog/privacy-discussions-web-bots#meet-seamless-web-bots), [integrated payments](https://core.telegram.org/bots/payments) via **20** payment providers (with *Google Pay* and *Apple Pay* out of the box), delivering tailored push notifications to users, and [much more](https://core.telegram.org/bots). 6 | 7 | > Check out the [dedicated guide to Mini Apps](https://core.telegram.org/bots/webapps). 8 | -------------------------------------------------------------------------------- /telegram-web-app.js: -------------------------------------------------------------------------------- 1 | // WebView 2 | (function () { 3 | var eventHandlers = {}; 4 | 5 | var locationHash = ''; 6 | try { 7 | locationHash = location.hash.toString(); 8 | } catch (e) {} 9 | 10 | var initParams = urlParseHashParams(locationHash); 11 | var storedParams = sessionStorageGet('initParams'); 12 | if (storedParams) { 13 | for (var key in storedParams) { 14 | if (typeof initParams[key] === 'undefined') { 15 | initParams[key] = storedParams[key]; 16 | } 17 | } 18 | } 19 | sessionStorageSet('initParams', initParams); 20 | 21 | var isIframe = false, iFrameStyle; 22 | try { 23 | isIframe = (window.parent != null && window != window.parent); 24 | if (isIframe) { 25 | window.addEventListener('message', function (event) { 26 | if (event.source !== window.parent) return; 27 | try { 28 | var dataParsed = JSON.parse(event.data); 29 | } catch (e) { 30 | return; 31 | } 32 | if (!dataParsed || !dataParsed.eventType) { 33 | return; 34 | } 35 | if (dataParsed.eventType == 'set_custom_style') { 36 | if (event.origin === 'https://web.telegram.org') { 37 | iFrameStyle.innerHTML = dataParsed.eventData; 38 | } 39 | } else if (dataParsed.eventType == 'reload_iframe') { 40 | try { 41 | window.parent.postMessage(JSON.stringify({eventType: 'iframe_will_reload'}), '*'); 42 | } catch (e) {} 43 | location.reload(); 44 | } else { 45 | receiveEvent(dataParsed.eventType, dataParsed.eventData); 46 | } 47 | }); 48 | iFrameStyle = document.createElement('style'); 49 | document.head.appendChild(iFrameStyle); 50 | try { 51 | window.parent.postMessage(JSON.stringify({eventType: 'iframe_ready', eventData: {reload_supported: true}}), '*'); 52 | } catch (e) {} 53 | } 54 | } catch (e) {} 55 | 56 | function urlSafeDecode(urlencoded) { 57 | try { 58 | urlencoded = urlencoded.replace(/\+/g, '%20'); 59 | return decodeURIComponent(urlencoded); 60 | } catch (e) { 61 | return urlencoded; 62 | } 63 | } 64 | 65 | function urlParseHashParams(locationHash) { 66 | locationHash = locationHash.replace(/^#/, ''); 67 | var params = {}; 68 | if (!locationHash.length) { 69 | return params; 70 | } 71 | if (locationHash.indexOf('=') < 0 && locationHash.indexOf('?') < 0) { 72 | params._path = urlSafeDecode(locationHash); 73 | return params; 74 | } 75 | var qIndex = locationHash.indexOf('?'); 76 | if (qIndex >= 0) { 77 | var pathParam = locationHash.substr(0, qIndex); 78 | params._path = urlSafeDecode(pathParam); 79 | locationHash = locationHash.substr(qIndex + 1); 80 | } 81 | var query_params = urlParseQueryString(locationHash); 82 | for (var k in query_params) { 83 | params[k] = query_params[k]; 84 | } 85 | return params; 86 | } 87 | 88 | function urlParseQueryString(queryString) { 89 | var params = {}; 90 | if (!queryString.length) { 91 | return params; 92 | } 93 | var queryStringParams = queryString.split('&'); 94 | var i, param, paramName, paramValue; 95 | for (i = 0; i < queryStringParams.length; i++) { 96 | param = queryStringParams[i].split('='); 97 | paramName = urlSafeDecode(param[0]); 98 | paramValue = param[1] == null ? null : urlSafeDecode(param[1]); 99 | params[paramName] = paramValue; 100 | } 101 | return params; 102 | } 103 | 104 | // Telegram apps will implement this logic to add service params (e.g. tgShareScoreUrl) to game URL 105 | function urlAppendHashParams(url, addHash) { 106 | // url looks like 'https://game.com/path?query=1#hash' 107 | // addHash looks like 'tgShareScoreUrl=' + encodeURIComponent('tgb://share_game_score?hash=very_long_hash123') 108 | 109 | var ind = url.indexOf('#'); 110 | if (ind < 0) { 111 | // https://game.com/path -> https://game.com/path#tgShareScoreUrl=etc 112 | return url + '#' + addHash; 113 | } 114 | var curHash = url.substr(ind + 1); 115 | if (curHash.indexOf('=') >= 0 || curHash.indexOf('?') >= 0) { 116 | // https://game.com/#hash=1 -> https://game.com/#hash=1&tgShareScoreUrl=etc 117 | // https://game.com/#path?query -> https://game.com/#path?query&tgShareScoreUrl=etc 118 | return url + '&' + addHash; 119 | } 120 | // https://game.com/#hash -> https://game.com/#hash?tgShareScoreUrl=etc 121 | if (curHash.length > 0) { 122 | return url + '?' + addHash; 123 | } 124 | // https://game.com/# -> https://game.com/#tgShareScoreUrl=etc 125 | return url + addHash; 126 | } 127 | 128 | function postEvent(eventType, callback, eventData) { 129 | if (!callback) { 130 | callback = function () {}; 131 | } 132 | if (eventData === undefined) { 133 | eventData = ''; 134 | } 135 | console.log('[Telegram.WebView] > postEvent', eventType, eventData); 136 | 137 | if (window.TelegramWebviewProxy !== undefined) { 138 | TelegramWebviewProxy.postEvent(eventType, JSON.stringify(eventData)); 139 | callback(); 140 | } 141 | else if (window.external && 'notify' in window.external) { 142 | window.external.notify(JSON.stringify({eventType: eventType, eventData: eventData})); 143 | callback(); 144 | } 145 | else if (isIframe) { 146 | try { 147 | var trustedTarget = 'https://web.telegram.org'; 148 | // For now we don't restrict target, for testing purposes 149 | trustedTarget = '*'; 150 | window.parent.postMessage(JSON.stringify({eventType: eventType, eventData: eventData}), trustedTarget); 151 | callback(); 152 | } catch (e) { 153 | callback(e); 154 | } 155 | } 156 | else { 157 | callback({notAvailable: true}); 158 | } 159 | }; 160 | 161 | function receiveEvent(eventType, eventData) { 162 | console.log('[Telegram.WebView] < receiveEvent', eventType, eventData); 163 | callEventCallbacks(eventType, function(callback) { 164 | callback(eventType, eventData); 165 | }); 166 | } 167 | 168 | function callEventCallbacks(eventType, func) { 169 | var curEventHandlers = eventHandlers[eventType]; 170 | if (curEventHandlers === undefined || 171 | !curEventHandlers.length) { 172 | return; 173 | } 174 | for (var i = 0; i < curEventHandlers.length; i++) { 175 | try { 176 | func(curEventHandlers[i]); 177 | } catch (e) {} 178 | } 179 | } 180 | 181 | function onEvent(eventType, callback) { 182 | if (eventHandlers[eventType] === undefined) { 183 | eventHandlers[eventType] = []; 184 | } 185 | var index = eventHandlers[eventType].indexOf(callback); 186 | if (index === -1) { 187 | eventHandlers[eventType].push(callback); 188 | } 189 | }; 190 | 191 | function offEvent(eventType, callback) { 192 | if (eventHandlers[eventType] === undefined) { 193 | return; 194 | } 195 | var index = eventHandlers[eventType].indexOf(callback); 196 | if (index === -1) { 197 | return; 198 | } 199 | eventHandlers[eventType].splice(index, 1); 200 | }; 201 | 202 | function openProtoUrl(url) { 203 | if (!url.match(/^(web\+)?tgb?:\/\/./)) { 204 | return false; 205 | } 206 | var useIframe = navigator.userAgent.match(/iOS|iPhone OS|iPhone|iPod|iPad/i) ? true : false; 207 | if (useIframe) { 208 | var iframeContEl = document.getElementById('tgme_frame_cont') || document.body; 209 | var iframeEl = document.createElement('iframe'); 210 | iframeContEl.appendChild(iframeEl); 211 | var pageHidden = false; 212 | var enableHidden = function () { 213 | pageHidden = true; 214 | }; 215 | window.addEventListener('pagehide', enableHidden, false); 216 | window.addEventListener('blur', enableHidden, false); 217 | if (iframeEl !== null) { 218 | iframeEl.src = url; 219 | } 220 | setTimeout(function() { 221 | if (!pageHidden) { 222 | window.location = url; 223 | } 224 | window.removeEventListener('pagehide', enableHidden, false); 225 | window.removeEventListener('blur', enableHidden, false); 226 | }, 2000); 227 | } 228 | else { 229 | window.location = url; 230 | } 231 | return true; 232 | } 233 | 234 | function sessionStorageSet(key, value) { 235 | try { 236 | window.sessionStorage.setItem('__telegram__' + key, JSON.stringify(value)); 237 | return true; 238 | } catch(e) {} 239 | return false; 240 | } 241 | function sessionStorageGet(key) { 242 | try { 243 | return JSON.parse(window.sessionStorage.getItem('__telegram__' + key)); 244 | } catch(e) {} 245 | return null; 246 | } 247 | 248 | if (!window.Telegram) { 249 | window.Telegram = {}; 250 | } 251 | window.Telegram.WebView = { 252 | initParams: initParams, 253 | isIframe: isIframe, 254 | onEvent: onEvent, 255 | offEvent: offEvent, 256 | postEvent: postEvent, 257 | receiveEvent: receiveEvent, 258 | callEventCallbacks: callEventCallbacks 259 | }; 260 | 261 | window.Telegram.Utils = { 262 | urlSafeDecode: urlSafeDecode, 263 | urlParseQueryString: urlParseQueryString, 264 | urlParseHashParams: urlParseHashParams, 265 | urlAppendHashParams: urlAppendHashParams, 266 | sessionStorageSet: sessionStorageSet, 267 | sessionStorageGet: sessionStorageGet 268 | }; 269 | 270 | // For Windows Phone app 271 | window.TelegramGameProxy_receiveEvent = receiveEvent; 272 | 273 | // App backward compatibility 274 | window.TelegramGameProxy = { 275 | receiveEvent: receiveEvent 276 | }; 277 | })(); 278 | 279 | // WebApp 280 | (function () { 281 | var Utils = window.Telegram.Utils; 282 | var WebView = window.Telegram.WebView; 283 | var initParams = WebView.initParams; 284 | var isIframe = WebView.isIframe; 285 | 286 | var WebApp = {}; 287 | var webAppInitData = '', webAppInitDataUnsafe = {}; 288 | var themeParams = {}, colorScheme = 'light'; 289 | var webAppVersion = '6.0'; 290 | var webAppPlatform = 'unknown'; 291 | 292 | if (initParams.tgWebAppData && initParams.tgWebAppData.length) { 293 | webAppInitData = initParams.tgWebAppData; 294 | webAppInitDataUnsafe = Utils.urlParseQueryString(webAppInitData); 295 | for (var key in webAppInitDataUnsafe) { 296 | var val = webAppInitDataUnsafe[key]; 297 | try { 298 | if (val.substr(0, 1) == '{' && val.substr(-1) == '}' || 299 | val.substr(0, 1) == '[' && val.substr(-1) == ']') { 300 | webAppInitDataUnsafe[key] = JSON.parse(val); 301 | } 302 | } catch (e) {} 303 | } 304 | } 305 | if (initParams.tgWebAppThemeParams && initParams.tgWebAppThemeParams.length) { 306 | var themeParamsRaw = initParams.tgWebAppThemeParams; 307 | try { 308 | var theme_params = JSON.parse(themeParamsRaw); 309 | if (theme_params) { 310 | setThemeParams(theme_params); 311 | } 312 | } catch (e) {} 313 | } 314 | var theme_params = Utils.sessionStorageGet('themeParams'); 315 | if (theme_params) { 316 | setThemeParams(theme_params); 317 | } 318 | if (initParams.tgWebAppVersion) { 319 | webAppVersion = initParams.tgWebAppVersion; 320 | } 321 | if (initParams.tgWebAppPlatform) { 322 | webAppPlatform = initParams.tgWebAppPlatform; 323 | } 324 | 325 | function onThemeChanged(eventType, eventData) { 326 | if (eventData.theme_params) { 327 | setThemeParams(eventData.theme_params); 328 | window.Telegram.WebApp.MainButton.setParams({}); 329 | updateBackgroundColor(); 330 | receiveWebViewEvent('themeChanged'); 331 | } 332 | } 333 | 334 | var lastWindowHeight = window.innerHeight; 335 | function onViewportChanged(eventType, eventData) { 336 | if (eventData.height) { 337 | window.removeEventListener('resize', onWindowResize); 338 | setViewportHeight(eventData); 339 | } 340 | } 341 | 342 | function onWindowResize(e) { 343 | if (lastWindowHeight != window.innerHeight) { 344 | lastWindowHeight = window.innerHeight; 345 | receiveWebViewEvent('viewportChanged', { 346 | isStateStable: true 347 | }); 348 | } 349 | } 350 | 351 | function linkHandler(e) { 352 | if (e.metaKey || e.ctrlKey) return; 353 | var el = e.target; 354 | while (el.tagName != 'A' && el.parentNode) { 355 | el = el.parentNode; 356 | } 357 | if (el.tagName == 'A' && 358 | el.target != '_blank' && 359 | (el.protocol == 'http:' || el.protocol == 'https:') && 360 | el.hostname == 't.me') { 361 | WebApp.openTgLink(el.href); 362 | e.preventDefault(); 363 | } 364 | } 365 | 366 | function strTrim(str) { 367 | return str.toString().replace(/^\s+|\s+$/g, ''); 368 | } 369 | 370 | function receiveWebViewEvent(eventType) { 371 | var args = Array.prototype.slice.call(arguments); 372 | eventType = args.shift(); 373 | WebView.callEventCallbacks('webview:' + eventType, function(callback) { 374 | callback.apply(WebApp, args); 375 | }); 376 | } 377 | 378 | function onWebViewEvent(eventType, callback) { 379 | WebView.onEvent('webview:' + eventType, callback); 380 | }; 381 | 382 | function offWebViewEvent(eventType, callback) { 383 | WebView.offEvent('webview:' + eventType, callback); 384 | }; 385 | 386 | function setCssProperty(name, value) { 387 | var root = document.documentElement; 388 | if (root && root.style && root.style.setProperty) { 389 | root.style.setProperty('--tg-' + name, value); 390 | } 391 | } 392 | 393 | function setThemeParams(theme_params) { 394 | // temp iOS fix 395 | if (theme_params.bg_color == '#1c1c1d' && 396 | theme_params.bg_color == theme_params.secondary_bg_color) { 397 | theme_params.secondary_bg_color = '#2c2c2e'; 398 | } 399 | var color; 400 | for (var key in theme_params) { 401 | if (color = parseColorToHex(theme_params[key])) { 402 | themeParams[key] = color; 403 | if (key == 'bg_color') { 404 | colorScheme = isColorDark(color) ? 'dark' : 'light' 405 | setCssProperty('color-scheme', colorScheme); 406 | } 407 | key = 'theme-' + key.split('_').join('-'); 408 | setCssProperty(key, color); 409 | } 410 | } 411 | Utils.sessionStorageSet('themeParams', themeParams); 412 | } 413 | 414 | var webAppCallbacks = {}; 415 | function generateCallbackId(len) { 416 | var tries = 100; 417 | while (--tries) { 418 | var id = '', chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', chars_len = chars.length; 419 | for (var i = 0; i < len; i++) { 420 | id += chars[Math.floor(Math.random() * chars_len)]; 421 | } 422 | if (!webAppCallbacks[id]) { 423 | webAppCallbacks[id] = {}; 424 | return id; 425 | } 426 | } 427 | throw Error('WebAppCallbackIdGenerateFailed'); 428 | } 429 | 430 | var viewportHeight = false, viewportStableHeight = false, isExpanded = true; 431 | function setViewportHeight(data) { 432 | if (typeof data !== 'undefined') { 433 | isExpanded = !!data.is_expanded; 434 | viewportHeight = data.height; 435 | if (data.is_state_stable) { 436 | viewportStableHeight = data.height; 437 | } 438 | receiveWebViewEvent('viewportChanged', { 439 | isStateStable: !!data.is_state_stable 440 | }); 441 | } 442 | var height, stable_height; 443 | if (viewportHeight !== false) { 444 | height = (viewportHeight - mainButtonHeight) + 'px'; 445 | } else { 446 | height = mainButtonHeight ? 'calc(100vh - ' + mainButtonHeight + 'px)' : '100vh'; 447 | } 448 | if (viewportStableHeight !== false) { 449 | stable_height = (viewportStableHeight - mainButtonHeight) + 'px'; 450 | } else { 451 | stable_height = mainButtonHeight ? 'calc(100vh - ' + mainButtonHeight + 'px)' : '100vh'; 452 | } 453 | setCssProperty('viewport-height', height); 454 | setCssProperty('viewport-stable-height', stable_height); 455 | } 456 | 457 | var isClosingConfirmationEnabled = false; 458 | function setClosingConfirmation(need_confirmation) { 459 | if (!versionAtLeast('6.2')) { 460 | console.warn('[Telegram.WebApp] Closing confirmation is not supported in version ' + webAppVersion); 461 | return; 462 | } 463 | isClosingConfirmationEnabled = !!need_confirmation; 464 | WebView.postEvent('web_app_setup_closing_behavior', false, {need_confirmation: isClosingConfirmationEnabled}); 465 | } 466 | 467 | var headerColorKey = 'bg_color', headerColor = null; 468 | function getHeaderColor() { 469 | if (headerColorKey == 'secondary_bg_color') { 470 | return themeParams.secondary_bg_color; 471 | } else if (headerColorKey == 'bg_color') { 472 | return themeParams.bg_color; 473 | } 474 | return headerColor; 475 | } 476 | function setHeaderColor(color) { 477 | if (!versionAtLeast('6.1')) { 478 | console.warn('[Telegram.WebApp] Header color is not supported in version ' + webAppVersion); 479 | return; 480 | } 481 | if (!versionAtLeast('6.9')) { 482 | if (themeParams.bg_color && 483 | themeParams.bg_color == color) { 484 | color = 'bg_color'; 485 | } else if (themeParams.secondary_bg_color && 486 | themeParams.secondary_bg_color == color) { 487 | color = 'secondary_bg_color'; 488 | } 489 | } 490 | var head_color = null, color_key = null; 491 | if (color == 'bg_color' || color == 'secondary_bg_color') { 492 | color_key = color; 493 | } else if (versionAtLeast('6.9')) { 494 | head_color = parseColorToHex(color); 495 | if (!head_color) { 496 | console.error('[Telegram.WebApp] Header color format is invalid', color); 497 | throw Error('WebAppHeaderColorInvalid'); 498 | } 499 | } 500 | if (!versionAtLeast('6.9') && 501 | color_key != 'bg_color' && 502 | color_key != 'secondary_bg_color') { 503 | console.error('[Telegram.WebApp] Header color key should be one of Telegram.WebApp.themeParams.bg_color, Telegram.WebApp.themeParams.secondary_bg_color, \'bg_color\', \'secondary_bg_color\'', color); 504 | throw Error('WebAppHeaderColorKeyInvalid'); 505 | } 506 | headerColorKey = color_key; 507 | headerColor = head_color; 508 | updateHeaderColor(); 509 | } 510 | var appHeaderColorKey = null, appHeaderColor = null; 511 | function updateHeaderColor() { 512 | if (appHeaderColorKey != headerColorKey || 513 | appHeaderColor != headerColor) { 514 | appHeaderColorKey = headerColorKey; 515 | appHeaderColor = headerColor; 516 | if (appHeaderColor) { 517 | WebView.postEvent('web_app_set_header_color', false, {color: headerColor}); 518 | } else { 519 | WebView.postEvent('web_app_set_header_color', false, {color_key: headerColorKey}); 520 | } 521 | } 522 | } 523 | 524 | var backgroundColor = 'bg_color'; 525 | function getBackgroundColor() { 526 | if (backgroundColor == 'secondary_bg_color') { 527 | return themeParams.secondary_bg_color; 528 | } else if (backgroundColor == 'bg_color') { 529 | return themeParams.bg_color; 530 | } 531 | return backgroundColor; 532 | } 533 | function setBackgroundColor(color) { 534 | if (!versionAtLeast('6.1')) { 535 | console.warn('[Telegram.WebApp] Background color is not supported in version ' + webAppVersion); 536 | return; 537 | } 538 | var bg_color; 539 | if (color == 'bg_color' || color == 'secondary_bg_color') { 540 | bg_color = color; 541 | } else { 542 | bg_color = parseColorToHex(color); 543 | if (!bg_color) { 544 | console.error('[Telegram.WebApp] Background color format is invalid', color); 545 | throw Error('WebAppBackgroundColorInvalid'); 546 | } 547 | } 548 | backgroundColor = bg_color; 549 | updateBackgroundColor(); 550 | } 551 | var appBackgroundColor = null; 552 | function updateBackgroundColor() { 553 | var color = getBackgroundColor(); 554 | if (appBackgroundColor != color) { 555 | appBackgroundColor = color; 556 | WebView.postEvent('web_app_set_background_color', false, {color: color}); 557 | } 558 | } 559 | 560 | 561 | function parseColorToHex(color) { 562 | color += ''; 563 | var match; 564 | if (match = /^\s*#([0-9a-f]{6})\s*$/i.exec(color)) { 565 | return '#' + match[1].toLowerCase(); 566 | } 567 | else if (match = /^\s*#([0-9a-f])([0-9a-f])([0-9a-f])\s*$/i.exec(color)) { 568 | return ('#' + match[1] + match[1] + match[2] + match[2] + match[3] + match[3]).toLowerCase(); 569 | } 570 | else if (match = /^\s*rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)\s*$/.exec(color)) { 571 | var r = parseInt(match[1]), g = parseInt(match[2]), b = parseInt(match[3]); 572 | r = (r < 16 ? '0' : '') + r.toString(16); 573 | g = (g < 16 ? '0' : '') + g.toString(16); 574 | b = (b < 16 ? '0' : '') + b.toString(16); 575 | return '#' + r + g + b; 576 | } 577 | return false; 578 | } 579 | 580 | function isColorDark(rgb) { 581 | rgb = rgb.replace(/[\s#]/g, ''); 582 | if (rgb.length == 3) { 583 | rgb = rgb[0] + rgb[0] + rgb[1] + rgb[1] + rgb[2] + rgb[2]; 584 | } 585 | var r = parseInt(rgb.substr(0, 2), 16); 586 | var g = parseInt(rgb.substr(2, 2), 16); 587 | var b = parseInt(rgb.substr(4, 2), 16); 588 | var hsp = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)); 589 | return hsp < 120; 590 | } 591 | 592 | function versionCompare(v1, v2) { 593 | if (typeof v1 !== 'string') v1 = ''; 594 | if (typeof v2 !== 'string') v2 = ''; 595 | v1 = v1.replace(/^\s+|\s+$/g, '').split('.'); 596 | v2 = v2.replace(/^\s+|\s+$/g, '').split('.'); 597 | var a = Math.max(v1.length, v2.length), i, p1, p2; 598 | for (i = 0; i < a; i++) { 599 | p1 = parseInt(v1[i]) || 0; 600 | p2 = parseInt(v2[i]) || 0; 601 | if (p1 == p2) continue; 602 | if (p1 > p2) return 1; 603 | return -1; 604 | } 605 | return 0; 606 | } 607 | 608 | function versionAtLeast(ver) { 609 | return versionCompare(webAppVersion, ver) >= 0; 610 | } 611 | 612 | function byteLength(str) { 613 | if (window.Blob) { 614 | try { return new Blob([str]).size; } catch (e) {} 615 | } 616 | var s = str.length; 617 | for (var i=str.length-1; i>=0; i--) { 618 | var code = str.charCodeAt(i); 619 | if (code > 0x7f && code <= 0x7ff) s++; 620 | else if (code > 0x7ff && code <= 0xffff) s+=2; 621 | if (code >= 0xdc00 && code <= 0xdfff) i--; 622 | } 623 | return s; 624 | } 625 | 626 | var BackButton = (function() { 627 | var isVisible = false; 628 | 629 | var backButton = {}; 630 | Object.defineProperty(backButton, 'isVisible', { 631 | set: function(val){ setParams({is_visible: val}); }, 632 | get: function(){ return isVisible; }, 633 | enumerable: true 634 | }); 635 | 636 | var curButtonState = null; 637 | 638 | WebView.onEvent('back_button_pressed', onBackButtonPressed); 639 | 640 | function onBackButtonPressed() { 641 | receiveWebViewEvent('backButtonClicked'); 642 | } 643 | 644 | function buttonParams() { 645 | return {is_visible: isVisible}; 646 | } 647 | 648 | function buttonState(btn_params) { 649 | if (typeof btn_params === 'undefined') { 650 | btn_params = buttonParams(); 651 | } 652 | return JSON.stringify(btn_params); 653 | } 654 | 655 | function buttonCheckVersion() { 656 | if (!versionAtLeast('6.1')) { 657 | console.warn('[Telegram.WebApp] BackButton is not supported in version ' + webAppVersion); 658 | return false; 659 | } 660 | return true; 661 | } 662 | 663 | function updateButton() { 664 | var btn_params = buttonParams(); 665 | var btn_state = buttonState(btn_params); 666 | if (curButtonState === btn_state) { 667 | return; 668 | } 669 | curButtonState = btn_state; 670 | WebView.postEvent('web_app_setup_back_button', false, btn_params); 671 | } 672 | 673 | function setParams(params) { 674 | if (!buttonCheckVersion()) { 675 | return backButton; 676 | } 677 | if (typeof params.is_visible !== 'undefined') { 678 | isVisible = !!params.is_visible; 679 | } 680 | updateButton(); 681 | return backButton; 682 | } 683 | 684 | backButton.onClick = function(callback) { 685 | if (buttonCheckVersion()) { 686 | onWebViewEvent('backButtonClicked', callback); 687 | } 688 | return backButton; 689 | }; 690 | backButton.offClick = function(callback) { 691 | if (buttonCheckVersion()) { 692 | offWebViewEvent('backButtonClicked', callback); 693 | } 694 | return backButton; 695 | }; 696 | backButton.show = function() { 697 | return setParams({is_visible: true}); 698 | }; 699 | backButton.hide = function() { 700 | return setParams({is_visible: false}); 701 | }; 702 | return backButton; 703 | })(); 704 | 705 | var mainButtonHeight = 0; 706 | var MainButton = (function() { 707 | var isVisible = false; 708 | var isActive = true; 709 | var isProgressVisible = false; 710 | var buttonText = 'CONTINUE'; 711 | var buttonColor = false; 712 | var buttonTextColor = false; 713 | 714 | var mainButton = {}; 715 | Object.defineProperty(mainButton, 'text', { 716 | set: function(val){ mainButton.setParams({text: val}); }, 717 | get: function(){ return buttonText; }, 718 | enumerable: true 719 | }); 720 | Object.defineProperty(mainButton, 'color', { 721 | set: function(val){ mainButton.setParams({color: val}); }, 722 | get: function(){ return buttonColor || themeParams.button_color || '#2481cc'; }, 723 | enumerable: true 724 | }); 725 | Object.defineProperty(mainButton, 'textColor', { 726 | set: function(val){ mainButton.setParams({text_color: val}); }, 727 | get: function(){ return buttonTextColor || themeParams.button_text_color || '#ffffff'; }, 728 | enumerable: true 729 | }); 730 | Object.defineProperty(mainButton, 'isVisible', { 731 | set: function(val){ mainButton.setParams({is_visible: val}); }, 732 | get: function(){ return isVisible; }, 733 | enumerable: true 734 | }); 735 | Object.defineProperty(mainButton, 'isProgressVisible', { 736 | get: function(){ return isProgressVisible; }, 737 | enumerable: true 738 | }); 739 | Object.defineProperty(mainButton, 'isActive', { 740 | set: function(val){ mainButton.setParams({is_active: val}); }, 741 | get: function(){ return isActive; }, 742 | enumerable: true 743 | }); 744 | 745 | var curButtonState = null; 746 | 747 | WebView.onEvent('main_button_pressed', onMainButtonPressed); 748 | 749 | var debugBtn = null, debugBtnStyle = {}; 750 | if (initParams.tgWebAppDebug) { 751 | debugBtn = document.createElement('tg-main-button'); 752 | debugBtnStyle = { 753 | font: '600 14px/18px sans-serif', 754 | display: 'none', 755 | width: '100%', 756 | height: '48px', 757 | borderRadius: '0', 758 | background: 'no-repeat right center', 759 | position: 'fixed', 760 | left: '0', 761 | right: '0', 762 | bottom: '0', 763 | margin: '0', 764 | padding: '15px 20px', 765 | textAlign: 'center', 766 | boxSizing: 'border-box', 767 | zIndex: '10000' 768 | }; 769 | for (var k in debugBtnStyle) { 770 | debugBtn.style[k] = debugBtnStyle[k]; 771 | } 772 | document.addEventListener('DOMContentLoaded', function onDomLoaded(event) { 773 | document.removeEventListener('DOMContentLoaded', onDomLoaded); 774 | document.body.appendChild(debugBtn); 775 | debugBtn.addEventListener('click', onMainButtonPressed, false); 776 | }); 777 | } 778 | 779 | function onMainButtonPressed() { 780 | if (isActive) { 781 | receiveWebViewEvent('mainButtonClicked'); 782 | } 783 | } 784 | 785 | function buttonParams() { 786 | var color = mainButton.color; 787 | var text_color = mainButton.textColor; 788 | return isVisible ? { 789 | is_visible: true, 790 | is_active: isActive, 791 | is_progress_visible: isProgressVisible, 792 | text: buttonText, 793 | color: color, 794 | text_color: text_color 795 | } : {is_visible: false}; 796 | } 797 | 798 | function buttonState(btn_params) { 799 | if (typeof btn_params === 'undefined') { 800 | btn_params = buttonParams(); 801 | } 802 | return JSON.stringify(btn_params); 803 | } 804 | 805 | function updateButton() { 806 | var btn_params = buttonParams(); 807 | var btn_state = buttonState(btn_params); 808 | if (curButtonState === btn_state) { 809 | return; 810 | } 811 | curButtonState = btn_state; 812 | WebView.postEvent('web_app_setup_main_button', false, btn_params); 813 | if (initParams.tgWebAppDebug) { 814 | updateDebugButton(btn_params); 815 | } 816 | } 817 | 818 | function updateDebugButton(btn_params) { 819 | if (btn_params.is_visible) { 820 | debugBtn.style.display = 'block'; 821 | mainButtonHeight = 48; 822 | 823 | debugBtn.style.opacity = btn_params.is_active ? '1' : '0.8'; 824 | debugBtn.style.cursor = btn_params.is_active ? 'pointer' : 'auto'; 825 | debugBtn.disabled = !btn_params.is_active; 826 | debugBtn.innerText = btn_params.text; 827 | debugBtn.style.backgroundImage = btn_params.is_progress_visible ? "url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20viewport%3D%220%200%2048%2048%22%20width%3D%2248px%22%20height%3D%2248px%22%3E%3Ccircle%20cx%3D%2250%25%22%20cy%3D%2250%25%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222.25%22%20stroke-linecap%3D%22round%22%20fill%3D%22none%22%20stroke-dashoffset%3D%22106%22%20r%3D%229%22%20stroke-dasharray%3D%2256.52%22%20rotate%3D%22-90%22%3E%3Canimate%20attributeName%3D%22stroke-dashoffset%22%20attributeType%3D%22XML%22%20dur%3D%22360s%22%20from%3D%220%22%20to%3D%2212500%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3CanimateTransform%20attributeName%3D%22transform%22%20attributeType%3D%22XML%22%20type%3D%22rotate%22%20dur%3D%221s%22%20from%3D%22-90%2024%2024%22%20to%3D%22630%2024%2024%22%20repeatCount%3D%22indefinite%22%3E%3C%2FanimateTransform%3E%3C%2Fcircle%3E%3C%2Fsvg%3E')" : 'none'; 828 | debugBtn.style.backgroundColor = btn_params.color; 829 | debugBtn.style.color = btn_params.text_color; 830 | } else { 831 | debugBtn.style.display = 'none'; 832 | mainButtonHeight = 0; 833 | } 834 | if (document.documentElement) { 835 | document.documentElement.style.boxSizing = 'border-box'; 836 | document.documentElement.style.paddingBottom = mainButtonHeight + 'px'; 837 | } 838 | setViewportHeight(); 839 | } 840 | 841 | function setParams(params) { 842 | if (typeof params.text !== 'undefined') { 843 | var text = strTrim(params.text); 844 | if (!text.length) { 845 | console.error('[Telegram.WebApp] Main button text is required', params.text); 846 | throw Error('WebAppMainButtonParamInvalid'); 847 | } 848 | if (text.length > 64) { 849 | console.error('[Telegram.WebApp] Main button text is too long', text); 850 | throw Error('WebAppMainButtonParamInvalid'); 851 | } 852 | buttonText = text; 853 | } 854 | if (typeof params.color !== 'undefined') { 855 | if (params.color === false || 856 | params.color === null) { 857 | buttonColor = false; 858 | } else { 859 | var color = parseColorToHex(params.color); 860 | if (!color) { 861 | console.error('[Telegram.WebApp] Main button color format is invalid', params.color); 862 | throw Error('WebAppMainButtonParamInvalid'); 863 | } 864 | buttonColor = color; 865 | } 866 | } 867 | if (typeof params.text_color !== 'undefined') { 868 | if (params.text_color === false || 869 | params.text_color === null) { 870 | buttonTextColor = false; 871 | } else { 872 | var text_color = parseColorToHex(params.text_color); 873 | if (!text_color) { 874 | console.error('[Telegram.WebApp] Main button text color format is invalid', params.text_color); 875 | throw Error('WebAppMainButtonParamInvalid'); 876 | } 877 | buttonTextColor = text_color; 878 | } 879 | } 880 | if (typeof params.is_visible !== 'undefined') { 881 | if (params.is_visible && 882 | !mainButton.text.length) { 883 | console.error('[Telegram.WebApp] Main button text is required'); 884 | throw Error('WebAppMainButtonParamInvalid'); 885 | } 886 | isVisible = !!params.is_visible; 887 | } 888 | if (typeof params.is_active !== 'undefined') { 889 | isActive = !!params.is_active; 890 | } 891 | updateButton(); 892 | return mainButton; 893 | } 894 | 895 | mainButton.setText = function(text) { 896 | return mainButton.setParams({text: text}); 897 | }; 898 | mainButton.onClick = function(callback) { 899 | onWebViewEvent('mainButtonClicked', callback); 900 | return mainButton; 901 | }; 902 | mainButton.offClick = function(callback) { 903 | offWebViewEvent('mainButtonClicked', callback); 904 | return mainButton; 905 | }; 906 | mainButton.show = function() { 907 | return mainButton.setParams({is_visible: true}); 908 | }; 909 | mainButton.hide = function() { 910 | return mainButton.setParams({is_visible: false}); 911 | }; 912 | mainButton.enable = function() { 913 | return mainButton.setParams({is_active: true}); 914 | }; 915 | mainButton.disable = function() { 916 | return mainButton.setParams({is_active: false}); 917 | }; 918 | mainButton.showProgress = function(leaveActive) { 919 | isActive = !!leaveActive; 920 | isProgressVisible = true; 921 | updateButton(); 922 | return mainButton; 923 | }; 924 | mainButton.hideProgress = function() { 925 | if (!mainButton.isActive) { 926 | isActive = true; 927 | } 928 | isProgressVisible = false; 929 | updateButton(); 930 | return mainButton; 931 | } 932 | mainButton.setParams = setParams; 933 | return mainButton; 934 | })(); 935 | 936 | var SettingsButton = (function() { 937 | var isVisible = false; 938 | 939 | var settingsButton = {}; 940 | Object.defineProperty(settingsButton, 'isVisible', { 941 | set: function(val){ setParams({is_visible: val}); }, 942 | get: function(){ return isVisible; }, 943 | enumerable: true 944 | }); 945 | 946 | var curButtonState = null; 947 | 948 | WebView.onEvent('settings_button_pressed', onSettingsButtonPressed); 949 | 950 | function onSettingsButtonPressed() { 951 | receiveWebViewEvent('settingsButtonClicked'); 952 | } 953 | 954 | function buttonParams() { 955 | return {is_visible: isVisible}; 956 | } 957 | 958 | function buttonState(btn_params) { 959 | if (typeof btn_params === 'undefined') { 960 | btn_params = buttonParams(); 961 | } 962 | return JSON.stringify(btn_params); 963 | } 964 | 965 | function buttonCheckVersion() { 966 | if (!versionAtLeast('7.0')) { 967 | console.warn('[Telegram.WebApp] SettingsButton is not supported in version ' + webAppVersion); 968 | return false; 969 | } 970 | return true; 971 | } 972 | 973 | function updateButton() { 974 | var btn_params = buttonParams(); 975 | var btn_state = buttonState(btn_params); 976 | if (curButtonState === btn_state) { 977 | return; 978 | } 979 | curButtonState = btn_state; 980 | WebView.postEvent('web_app_setup_settings_button', false, btn_params); 981 | } 982 | 983 | function setParams(params) { 984 | if (!buttonCheckVersion()) { 985 | return settingsButton; 986 | } 987 | if (typeof params.is_visible !== 'undefined') { 988 | isVisible = !!params.is_visible; 989 | } 990 | updateButton(); 991 | return settingsButton; 992 | } 993 | 994 | settingsButton.onClick = function(callback) { 995 | if (buttonCheckVersion()) { 996 | onWebViewEvent('settingsButtonClicked', callback); 997 | } 998 | return settingsButton; 999 | }; 1000 | settingsButton.offClick = function(callback) { 1001 | if (buttonCheckVersion()) { 1002 | offWebViewEvent('settingsButtonClicked', callback); 1003 | } 1004 | return settingsButton; 1005 | }; 1006 | settingsButton.show = function() { 1007 | return setParams({is_visible: true}); 1008 | }; 1009 | settingsButton.hide = function() { 1010 | return setParams({is_visible: false}); 1011 | }; 1012 | return settingsButton; 1013 | })(); 1014 | 1015 | var HapticFeedback = (function() { 1016 | var hapticFeedback = {}; 1017 | 1018 | function triggerFeedback(params) { 1019 | if (!versionAtLeast('6.1')) { 1020 | console.warn('[Telegram.WebApp] HapticFeedback is not supported in version ' + webAppVersion); 1021 | return hapticFeedback; 1022 | } 1023 | if (params.type == 'impact') { 1024 | if (params.impact_style != 'light' && 1025 | params.impact_style != 'medium' && 1026 | params.impact_style != 'heavy' && 1027 | params.impact_style != 'rigid' && 1028 | params.impact_style != 'soft') { 1029 | console.error('[Telegram.WebApp] Haptic impact style is invalid', params.impact_style); 1030 | throw Error('WebAppHapticImpactStyleInvalid'); 1031 | } 1032 | } else if (params.type == 'notification') { 1033 | if (params.notification_type != 'error' && 1034 | params.notification_type != 'success' && 1035 | params.notification_type != 'warning') { 1036 | console.error('[Telegram.WebApp] Haptic notification type is invalid', params.notification_type); 1037 | throw Error('WebAppHapticNotificationTypeInvalid'); 1038 | } 1039 | } else if (params.type == 'selection_change') { 1040 | // no params needed 1041 | } else { 1042 | console.error('[Telegram.WebApp] Haptic feedback type is invalid', params.type); 1043 | throw Error('WebAppHapticFeedbackTypeInvalid'); 1044 | } 1045 | WebView.postEvent('web_app_trigger_haptic_feedback', false, params); 1046 | return hapticFeedback; 1047 | } 1048 | 1049 | hapticFeedback.impactOccurred = function(style) { 1050 | return triggerFeedback({type: 'impact', impact_style: style}); 1051 | }; 1052 | hapticFeedback.notificationOccurred = function(type) { 1053 | return triggerFeedback({type: 'notification', notification_type: type}); 1054 | }; 1055 | hapticFeedback.selectionChanged = function() { 1056 | return triggerFeedback({type: 'selection_change'}); 1057 | }; 1058 | return hapticFeedback; 1059 | })(); 1060 | 1061 | var CloudStorage = (function() { 1062 | var cloudStorage = {}; 1063 | 1064 | function invokeStorageMethod(method, params, callback) { 1065 | if (!versionAtLeast('6.9')) { 1066 | console.error('[Telegram.WebApp] CloudStorage is not supported in version ' + webAppVersion); 1067 | throw Error('WebAppMethodUnsupported'); 1068 | } 1069 | invokeCustomMethod(method, params, callback); 1070 | return cloudStorage; 1071 | } 1072 | 1073 | cloudStorage.setItem = function(key, value, callback) { 1074 | return invokeStorageMethod('saveStorageValue', {key: key, value: value}, callback); 1075 | }; 1076 | cloudStorage.getItem = function(key, callback) { 1077 | return cloudStorage.getItems([key], callback ? function(err, res) { 1078 | if (err) callback(err); 1079 | else callback(null, res[key]); 1080 | } : null); 1081 | }; 1082 | cloudStorage.getItems = function(keys, callback) { 1083 | return invokeStorageMethod('getStorageValues', {keys: keys}, callback); 1084 | }; 1085 | cloudStorage.removeItem = function(key, callback) { 1086 | return cloudStorage.removeItems([key], callback); 1087 | }; 1088 | cloudStorage.removeItems = function(keys, callback) { 1089 | return invokeStorageMethod('deleteStorageValues', {keys: keys}, callback); 1090 | }; 1091 | cloudStorage.getKeys = function(callback) { 1092 | return invokeStorageMethod('getStorageKeys', {}, callback); 1093 | }; 1094 | return cloudStorage; 1095 | })(); 1096 | 1097 | var webAppInvoices = {}; 1098 | function onInvoiceClosed(eventType, eventData) { 1099 | if (eventData.slug && webAppInvoices[eventData.slug]) { 1100 | var invoiceData = webAppInvoices[eventData.slug]; 1101 | delete webAppInvoices[eventData.slug]; 1102 | if (invoiceData.callback) { 1103 | invoiceData.callback(eventData.status); 1104 | } 1105 | receiveWebViewEvent('invoiceClosed', { 1106 | url: invoiceData.url, 1107 | status: eventData.status 1108 | }); 1109 | } 1110 | } 1111 | 1112 | var webAppPopupOpened = false; 1113 | function onPopupClosed(eventType, eventData) { 1114 | if (webAppPopupOpened) { 1115 | var popupData = webAppPopupOpened; 1116 | webAppPopupOpened = false; 1117 | var button_id = null; 1118 | if (typeof eventData.button_id !== 'undefined') { 1119 | button_id = eventData.button_id; 1120 | } 1121 | if (popupData.callback) { 1122 | popupData.callback(button_id); 1123 | } 1124 | receiveWebViewEvent('popupClosed', { 1125 | button_id: button_id 1126 | }); 1127 | } 1128 | } 1129 | 1130 | var webAppScanQrPopupOpened = false; 1131 | function onQrTextReceived(eventType, eventData) { 1132 | if (webAppScanQrPopupOpened) { 1133 | var popupData = webAppScanQrPopupOpened; 1134 | var data = null; 1135 | if (typeof eventData.data !== 'undefined') { 1136 | data = eventData.data; 1137 | } 1138 | if (popupData.callback) { 1139 | if (popupData.callback(data)) { 1140 | webAppScanQrPopupOpened = false; 1141 | WebView.postEvent('web_app_close_scan_qr_popup', false); 1142 | } 1143 | } 1144 | receiveWebViewEvent('qrTextReceived', { 1145 | data: data 1146 | }); 1147 | } 1148 | } 1149 | function onScanQrPopupClosed(eventType, eventData) { 1150 | webAppScanQrPopupOpened = false; 1151 | } 1152 | 1153 | function onClipboardTextReceived(eventType, eventData) { 1154 | if (eventData.req_id && webAppCallbacks[eventData.req_id]) { 1155 | var requestData = webAppCallbacks[eventData.req_id]; 1156 | delete webAppCallbacks[eventData.req_id]; 1157 | var data = null; 1158 | if (typeof eventData.data !== 'undefined') { 1159 | data = eventData.data; 1160 | } 1161 | if (requestData.callback) { 1162 | requestData.callback(data); 1163 | } 1164 | receiveWebViewEvent('clipboardTextReceived', { 1165 | data: data 1166 | }); 1167 | } 1168 | } 1169 | 1170 | var WebAppWriteAccessRequested = false; 1171 | function onWriteAccessRequested(eventType, eventData) { 1172 | if (WebAppWriteAccessRequested) { 1173 | var requestData = WebAppWriteAccessRequested; 1174 | WebAppWriteAccessRequested = false; 1175 | if (requestData.callback) { 1176 | requestData.callback(eventData.status == 'allowed'); 1177 | } 1178 | receiveWebViewEvent('writeAccessRequested', { 1179 | status: eventData.status 1180 | }); 1181 | } 1182 | } 1183 | 1184 | function getRequestedContact(callback, timeout) { 1185 | var reqTo, fallbackTo, reqDelay = 0; 1186 | var reqInvoke = function() { 1187 | invokeCustomMethod('getRequestedContact', {}, function(err, res) { 1188 | if (res && res.length) { 1189 | clearTimeout(fallbackTo); 1190 | callback(res); 1191 | } else { 1192 | reqDelay += 50; 1193 | reqTo = setTimeout(reqInvoke, reqDelay); 1194 | } 1195 | }); 1196 | }; 1197 | var fallbackInvoke = function() { 1198 | clearTimeout(reqTo); 1199 | callback(''); 1200 | }; 1201 | fallbackTo = setTimeout(fallbackInvoke, timeout); 1202 | reqInvoke(); 1203 | } 1204 | 1205 | var WebAppContactRequested = false; 1206 | function onPhoneRequested(eventType, eventData) { 1207 | if (WebAppContactRequested) { 1208 | var requestData = WebAppContactRequested; 1209 | WebAppContactRequested = false; 1210 | var requestSent = eventData.status == 'sent'; 1211 | var webViewEvent = { 1212 | status: eventData.status 1213 | }; 1214 | if (requestSent) { 1215 | getRequestedContact(function(res) { 1216 | if (res && res.length) { 1217 | webViewEvent.response = res; 1218 | webViewEvent.responseUnsafe = Utils.urlParseQueryString(res); 1219 | for (var key in webViewEvent.responseUnsafe) { 1220 | var val = webViewEvent.responseUnsafe[key]; 1221 | try { 1222 | if (val.substr(0, 1) == '{' && val.substr(-1) == '}' || 1223 | val.substr(0, 1) == '[' && val.substr(-1) == ']') { 1224 | webViewEvent.responseUnsafe[key] = JSON.parse(val); 1225 | } 1226 | } catch (e) {} 1227 | } 1228 | } 1229 | if (requestData.callback) { 1230 | requestData.callback(requestSent, webViewEvent); 1231 | } 1232 | receiveWebViewEvent('contactRequested', webViewEvent); 1233 | }, 3000); 1234 | } else { 1235 | if (requestData.callback) { 1236 | requestData.callback(requestSent, webViewEvent); 1237 | } 1238 | receiveWebViewEvent('contactRequested', webViewEvent); 1239 | } 1240 | } 1241 | } 1242 | 1243 | function onCustomMethodInvoked(eventType, eventData) { 1244 | if (eventData.req_id && webAppCallbacks[eventData.req_id]) { 1245 | var requestData = webAppCallbacks[eventData.req_id]; 1246 | delete webAppCallbacks[eventData.req_id]; 1247 | var res = null, err = null; 1248 | if (typeof eventData.result !== 'undefined') { 1249 | res = eventData.result; 1250 | } 1251 | if (typeof eventData.error !== 'undefined') { 1252 | err = eventData.error; 1253 | } 1254 | if (requestData.callback) { 1255 | requestData.callback(err, res); 1256 | } 1257 | } 1258 | } 1259 | 1260 | function invokeCustomMethod(method, params, callback) { 1261 | if (!versionAtLeast('6.9')) { 1262 | console.error('[Telegram.WebApp] Method invokeCustomMethod is not supported in version ' + webAppVersion); 1263 | throw Error('WebAppMethodUnsupported'); 1264 | } 1265 | var req_id = generateCallbackId(16); 1266 | var req_params = {req_id: req_id, method: method, params: params || {}}; 1267 | webAppCallbacks[req_id] = { 1268 | callback: callback 1269 | }; 1270 | WebView.postEvent('web_app_invoke_custom_method', false, req_params); 1271 | }; 1272 | 1273 | if (!window.Telegram) { 1274 | window.Telegram = {}; 1275 | } 1276 | 1277 | Object.defineProperty(WebApp, 'initData', { 1278 | get: function(){ return webAppInitData; }, 1279 | enumerable: true 1280 | }); 1281 | Object.defineProperty(WebApp, 'initDataUnsafe', { 1282 | get: function(){ return webAppInitDataUnsafe; }, 1283 | enumerable: true 1284 | }); 1285 | Object.defineProperty(WebApp, 'version', { 1286 | get: function(){ return webAppVersion; }, 1287 | enumerable: true 1288 | }); 1289 | Object.defineProperty(WebApp, 'platform', { 1290 | get: function(){ return webAppPlatform; }, 1291 | enumerable: true 1292 | }); 1293 | Object.defineProperty(WebApp, 'colorScheme', { 1294 | get: function(){ return colorScheme; }, 1295 | enumerable: true 1296 | }); 1297 | Object.defineProperty(WebApp, 'themeParams', { 1298 | get: function(){ return themeParams; }, 1299 | enumerable: true 1300 | }); 1301 | Object.defineProperty(WebApp, 'isExpanded', { 1302 | get: function(){ return isExpanded; }, 1303 | enumerable: true 1304 | }); 1305 | Object.defineProperty(WebApp, 'viewportHeight', { 1306 | get: function(){ return (viewportHeight === false ? window.innerHeight : viewportHeight) - mainButtonHeight; }, 1307 | enumerable: true 1308 | }); 1309 | Object.defineProperty(WebApp, 'viewportStableHeight', { 1310 | get: function(){ return (viewportStableHeight === false ? window.innerHeight : viewportStableHeight) - mainButtonHeight; }, 1311 | enumerable: true 1312 | }); 1313 | Object.defineProperty(WebApp, 'isClosingConfirmationEnabled', { 1314 | set: function(val){ setClosingConfirmation(val); }, 1315 | get: function(){ return isClosingConfirmationEnabled; }, 1316 | enumerable: true 1317 | }); 1318 | Object.defineProperty(WebApp, 'headerColor', { 1319 | set: function(val){ setHeaderColor(val); }, 1320 | get: function(){ return getHeaderColor(); }, 1321 | enumerable: true 1322 | }); 1323 | Object.defineProperty(WebApp, 'backgroundColor', { 1324 | set: function(val){ setBackgroundColor(val); }, 1325 | get: function(){ return getBackgroundColor(); }, 1326 | enumerable: true 1327 | }); 1328 | Object.defineProperty(WebApp, 'BackButton', { 1329 | value: BackButton, 1330 | enumerable: true 1331 | }); 1332 | Object.defineProperty(WebApp, 'MainButton', { 1333 | value: MainButton, 1334 | enumerable: true 1335 | }); 1336 | Object.defineProperty(WebApp, 'SettingsButton', { 1337 | value: SettingsButton, 1338 | enumerable: true 1339 | }); 1340 | Object.defineProperty(WebApp, 'HapticFeedback', { 1341 | value: HapticFeedback, 1342 | enumerable: true 1343 | }); 1344 | Object.defineProperty(WebApp, 'CloudStorage', { 1345 | value: CloudStorage, 1346 | enumerable: true 1347 | }); 1348 | WebApp.setHeaderColor = function(color_key) { 1349 | WebApp.headerColor = color_key; 1350 | }; 1351 | WebApp.setBackgroundColor = function(color) { 1352 | WebApp.backgroundColor = color; 1353 | }; 1354 | WebApp.enableClosingConfirmation = function() { 1355 | WebApp.isClosingConfirmationEnabled = true; 1356 | }; 1357 | WebApp.disableClosingConfirmation = function() { 1358 | WebApp.isClosingConfirmationEnabled = false; 1359 | }; 1360 | WebApp.isVersionAtLeast = function(ver) { 1361 | return versionAtLeast(ver); 1362 | }; 1363 | WebApp.onEvent = function(eventType, callback) { 1364 | onWebViewEvent(eventType, callback); 1365 | }; 1366 | WebApp.offEvent = function(eventType, callback) {offWebViewEvent(eventType, callback); 1367 | }; 1368 | WebApp.sendData = function (data) { 1369 | if (!data || !data.length) { 1370 | console.error('[Telegram.WebApp] Data is required', data); 1371 | throw Error('WebAppDataInvalid'); 1372 | } 1373 | if (byteLength(data) > 4096) { 1374 | console.error('[Telegram.WebApp] Data is too long', data); 1375 | throw Error('WebAppDataInvalid'); 1376 | } 1377 | WebView.postEvent('web_app_data_send', false, {data: data}); 1378 | }; 1379 | WebApp.switchInlineQuery = function (query, choose_chat_types) { 1380 | if (!versionAtLeast('6.6')) { 1381 | console.error('[Telegram.WebApp] Method switchInlineQuery is not supported in version ' + webAppVersion); 1382 | throw Error('WebAppMethodUnsupported'); 1383 | } 1384 | if (!initParams.tgWebAppBotInline) { 1385 | console.error('[Telegram.WebApp] Inline mode is disabled for this bot. Read more about inline mode: https://core.telegram.org/bots/inline'); 1386 | throw Error('WebAppInlineModeDisabled'); 1387 | } 1388 | query = query || ''; 1389 | if (query.length > 256) { 1390 | console.error('[Telegram.WebApp] Inline query is too long', query); 1391 | throw Error('WebAppInlineQueryInvalid'); 1392 | } 1393 | var chat_types = []; 1394 | if (choose_chat_types) { 1395 | if (!Array.isArray(choose_chat_types)) { 1396 | console.error('[Telegram.WebApp] Choose chat types should be an array', choose_chat_types); 1397 | throw Error('WebAppInlineChooseChatTypesInvalid'); 1398 | } 1399 | var good_types = {users: 1, bots: 1, groups: 1, channels: 1}; 1400 | for (var i = 0; i < choose_chat_types.length; i++) { 1401 | var chat_type = choose_chat_types[i]; 1402 | if (!good_types[chat_type]) { 1403 | console.error('[Telegram.WebApp] Choose chat type is invalid', chat_type); 1404 | throw Error('WebAppInlineChooseChatTypeInvalid'); 1405 | } 1406 | if (good_types[chat_type] != 2) { 1407 | good_types[chat_type] = 2; 1408 | chat_types.push(chat_type); 1409 | } 1410 | } 1411 | } 1412 | WebView.postEvent('web_app_switch_inline_query', false, {query: query, chat_types: chat_types}); 1413 | }; 1414 | WebApp.openLink = function (url, options) { 1415 | var a = document.createElement('A'); 1416 | a.href = url; 1417 | if (a.protocol != 'http:' && 1418 | a.protocol != 'https:') { 1419 | console.error('[Telegram.WebApp] Url protocol is not supported', url); 1420 | throw Error('WebAppTgUrlInvalid'); 1421 | } 1422 | var url = a.href; 1423 | options = options || {}; 1424 | if (versionAtLeast('6.1')) { 1425 | WebView.postEvent('web_app_open_link', false, {url: url, try_instant_view: versionAtLeast('6.4') && !!options.try_instant_view}); 1426 | } else { 1427 | window.open(url, '_blank'); 1428 | } 1429 | }; 1430 | WebApp.openTelegramLink = function (url) { 1431 | var a = document.createElement('A'); 1432 | a.href = url; 1433 | if (a.protocol != 'http:' && 1434 | a.protocol != 'https:') { 1435 | console.error('[Telegram.WebApp] Url protocol is not supported', url); 1436 | throw Error('WebAppTgUrlInvalid'); 1437 | } 1438 | if (a.hostname != 't.me') { 1439 | console.error('[Telegram.WebApp] Url host is not supported', url); 1440 | throw Error('WebAppTgUrlInvalid'); 1441 | } 1442 | var path_full = a.pathname + a.search; 1443 | if (isIframe || versionAtLeast('6.1')) { 1444 | WebView.postEvent('web_app_open_tg_link', false, {path_full: path_full}); 1445 | } else { 1446 | location.href = 'https://t.me' + path_full; 1447 | } 1448 | }; 1449 | WebApp.openInvoice = function (url, callback) { 1450 | var a = document.createElement('A'), match, slug; 1451 | a.href = url; 1452 | if (a.protocol != 'http:' && 1453 | a.protocol != 'https:' || 1454 | a.hostname != 't.me' || 1455 | !(match = a.pathname.match(/^\/(\$|invoice\/)([A-Za-z0-9\-_=]+)$/)) || 1456 | !(slug = match[2])) { 1457 | console.error('[Telegram.WebApp] Invoice url is invalid', url); 1458 | throw Error('WebAppInvoiceUrlInvalid'); 1459 | } 1460 | if (!versionAtLeast('6.1')) { 1461 | console.error('[Telegram.WebApp] Method openInvoice is not supported in version ' + webAppVersion); 1462 | throw Error('WebAppMethodUnsupported'); 1463 | } 1464 | if (webAppInvoices[slug]) { 1465 | console.error('[Telegram.WebApp] Invoice is already opened'); 1466 | throw Error('WebAppInvoiceOpened'); 1467 | } 1468 | webAppInvoices[slug] = { 1469 | url: url, 1470 | callback: callback 1471 | }; 1472 | WebView.postEvent('web_app_open_invoice', false, {slug: slug}); 1473 | }; 1474 | WebApp.showPopup = function (params, callback) { 1475 | if (!versionAtLeast('6.2')) { 1476 | console.error('[Telegram.WebApp] Method showPopup is not supported in version ' + webAppVersion); 1477 | throw Error('WebAppMethodUnsupported'); 1478 | } 1479 | if (webAppPopupOpened) { 1480 | console.error('[Telegram.WebApp] Popup is already opened'); 1481 | throw Error('WebAppPopupOpened'); 1482 | } 1483 | var title = ''; 1484 | var message = ''; 1485 | var buttons = []; 1486 | var popup_buttons = {}; 1487 | var popup_params = {}; 1488 | if (typeof params.title !== 'undefined') { 1489 | title = strTrim(params.title); 1490 | if (title.length > 64) { 1491 | console.error('[Telegram.WebApp] Popup title is too long', title); 1492 | throw Error('WebAppPopupParamInvalid'); 1493 | } 1494 | if (title.length > 0) { 1495 | popup_params.title = title; 1496 | } 1497 | } 1498 | if (typeof params.message !== 'undefined') { 1499 | message = strTrim(params.message); 1500 | } 1501 | if (!message.length) { 1502 | console.error('[Telegram.WebApp] Popup message is required', params.message); 1503 | throw Error('WebAppPopupParamInvalid'); 1504 | } 1505 | if (message.length > 256) { 1506 | console.error('[Telegram.WebApp] Popup message is too long', message); 1507 | throw Error('WebAppPopupParamInvalid'); 1508 | } 1509 | popup_params.message = message; 1510 | if (typeof params.buttons !== 'undefined') { 1511 | if (!Array.isArray(params.buttons)) { 1512 | console.error('[Telegram.WebApp] Popup buttons should be an array', params.buttons); 1513 | throw Error('WebAppPopupParamInvalid'); 1514 | } 1515 | for (var i = 0; i < params.buttons.length; i++) { 1516 | var button = params.buttons[i]; 1517 | var btn = {}; 1518 | var id = ''; 1519 | if (typeof button.id !== 'undefined') { 1520 | id = button.id.toString(); 1521 | if (id.length > 64) { 1522 | console.error('[Telegram.WebApp] Popup button id is too long', id); 1523 | throw Error('WebAppPopupParamInvalid'); 1524 | } 1525 | } 1526 | btn.id = id; 1527 | var button_type = button.type; 1528 | if (typeof button_type === 'undefined') { 1529 | button_type = 'default'; 1530 | } 1531 | btn.type = button_type; 1532 | if (button_type == 'ok' || 1533 | button_type == 'close' || 1534 | button_type == 'cancel') { 1535 | // no params needed 1536 | } else if (button_type == 'default' || 1537 | button_type == 'destructive') { 1538 | var text = ''; 1539 | if (typeof button.text !== 'undefined') { 1540 | text = strTrim(button.text); 1541 | } 1542 | if (!text.length) { 1543 | console.error('[Telegram.WebApp] Popup button text is required for type ' + button_type, button.text); 1544 | throw Error('WebAppPopupParamInvalid'); 1545 | } 1546 | if (text.length > 64) { 1547 | console.error('[Telegram.WebApp] Popup button text is too long', text); 1548 | throw Error('WebAppPopupParamInvalid'); 1549 | } 1550 | btn.text = text; 1551 | } else { 1552 | console.error('[Telegram.WebApp] Popup button type is invalid', button_type); 1553 | throw Error('WebAppPopupParamInvalid'); 1554 | } 1555 | buttons.push(btn); 1556 | } 1557 | } else { 1558 | buttons.push({id: '', type: 'close'}); 1559 | } 1560 | if (buttons.length < 1) { 1561 | console.error('[Telegram.WebApp] Popup should have at least one button'); 1562 | throw Error('WebAppPopupParamInvalid'); 1563 | } 1564 | if (buttons.length > 3) { 1565 | console.error('[Telegram.WebApp] Popup should not have more than 3 buttons'); 1566 | throw Error('WebAppPopupParamInvalid'); 1567 | } 1568 | popup_params.buttons = buttons; 1569 | 1570 | webAppPopupOpened = { 1571 | callback: callback 1572 | }; 1573 | WebView.postEvent('web_app_open_popup', false, popup_params); 1574 | }; 1575 | WebApp.showAlert = function (message, callback) { 1576 | WebApp.showPopup({ 1577 | message: message 1578 | }, callback ? function(){ callback(); } : null); 1579 | }; 1580 | WebApp.showConfirm = function (message, callback) { 1581 | WebApp.showPopup({ 1582 | message: message, 1583 | buttons: [ 1584 | {type: 'ok', id: 'ok'}, 1585 | {type: 'cancel'} 1586 | ] 1587 | }, callback ? function (button_id) { 1588 | callback(button_id == 'ok'); 1589 | } : null); 1590 | }; 1591 | WebApp.showScanQrPopup = function (params, callback) { 1592 | if (!versionAtLeast('6.4')) { 1593 | console.error('[Telegram.WebApp] Method showScanQrPopup is not supported in version ' + webAppVersion); 1594 | throw Error('WebAppMethodUnsupported'); 1595 | } 1596 | if (webAppScanQrPopupOpened) { 1597 | console.error('[Telegram.WebApp] Popup is already opened'); 1598 | throw Error('WebAppScanQrPopupOpened'); 1599 | } 1600 | var text = ''; 1601 | var popup_params = {}; 1602 | if (typeof params.text !== 'undefined') { 1603 | text = strTrim(params.text); 1604 | if (text.length > 64) { 1605 | console.error('[Telegram.WebApp] Scan QR popup text is too long', text); 1606 | throw Error('WebAppScanQrPopupParamInvalid'); 1607 | } 1608 | if (text.length > 0) { 1609 | popup_params.text = text; 1610 | } 1611 | } 1612 | 1613 | webAppScanQrPopupOpened = { 1614 | callback: callback 1615 | }; 1616 | WebView.postEvent('web_app_open_scan_qr_popup', false, popup_params); 1617 | }; 1618 | WebApp.closeScanQrPopup = function () { 1619 | if (!versionAtLeast('6.4')) { 1620 | console.error('[Telegram.WebApp] Method closeScanQrPopup is not supported in version ' + webAppVersion); 1621 | throw Error('WebAppMethodUnsupported'); 1622 | } 1623 | 1624 | webAppScanQrPopupOpened = false; 1625 | WebView.postEvent('web_app_close_scan_qr_popup', false); 1626 | }; 1627 | WebApp.readTextFromClipboard = function (callback) { 1628 | if (!versionAtLeast('6.4')) { 1629 | console.error('[Telegram.WebApp] Method readTextFromClipboard is not supported in version ' + webAppVersion); 1630 | throw Error('WebAppMethodUnsupported'); 1631 | } 1632 | var req_id = generateCallbackId(16); 1633 | var req_params = {req_id: req_id}; 1634 | webAppCallbacks[req_id] = { 1635 | callback: callback 1636 | }; 1637 | WebView.postEvent('web_app_read_text_from_clipboard', false, req_params); 1638 | }; 1639 | WebApp.requestWriteAccess = function (callback) { 1640 | if (!versionAtLeast('6.9')) { 1641 | console.error('[Telegram.WebApp] Method requestWriteAccess is not supported in version ' + webAppVersion); 1642 | throw Error('WebAppMethodUnsupported'); 1643 | } 1644 | if (WebAppWriteAccessRequested) { 1645 | console.error('[Telegram.WebApp] Write access is already requested'); 1646 | throw Error('WebAppWriteAccessRequested'); 1647 | } 1648 | WebAppWriteAccessRequested = { 1649 | callback: callback 1650 | }; 1651 | WebView.postEvent('web_app_request_write_access'); 1652 | }; 1653 | WebApp.requestContact = function (callback) { 1654 | if (!versionAtLeast('6.9')) { 1655 | console.error('[Telegram.WebApp] Method requestContact is not supported in version ' + webAppVersion); 1656 | throw Error('WebAppMethodUnsupported'); 1657 | } 1658 | if (WebAppContactRequested) { 1659 | console.error('[Telegram.WebApp] Contact is already requested'); 1660 | throw Error('WebAppContactRequested'); 1661 | } 1662 | WebAppContactRequested = { 1663 | callback: callback 1664 | }; 1665 | WebView.postEvent('web_app_request_phone'); 1666 | }; 1667 | WebApp.invokeCustomMethod = function (method, params, callback) { 1668 | invokeCustomMethod(method, params, callback); 1669 | }; 1670 | WebApp.ready = function () { 1671 | WebView.postEvent('web_app_ready'); 1672 | }; 1673 | WebApp.expand = function () { 1674 | WebView.postEvent('web_app_expand'); 1675 | }; 1676 | WebApp.close = function () { 1677 | WebView.postEvent('web_app_close'); 1678 | }; 1679 | 1680 | window.Telegram.WebApp = WebApp; 1681 | 1682 | updateHeaderColor(); 1683 | updateBackgroundColor(); 1684 | setViewportHeight(); 1685 | if (initParams.tgWebAppShowSettings) { 1686 | SettingsButton.show(); 1687 | } 1688 | 1689 | window.addEventListener('resize', onWindowResize); 1690 | if (isIframe) { 1691 | document.addEventListener('click', linkHandler); 1692 | } 1693 | 1694 | WebView.onEvent('theme_changed', onThemeChanged); 1695 | WebView.onEvent('viewport_changed', onViewportChanged); 1696 | WebView.onEvent('invoice_closed', onInvoiceClosed); 1697 | WebView.onEvent('popup_closed', onPopupClosed); 1698 | WebView.onEvent('qr_text_received', onQrTextReceived); 1699 | WebView.onEvent('scan_qr_popup_closed', onScanQrPopupClosed); 1700 | WebView.onEvent('clipboard_text_received', onClipboardTextReceived); 1701 | WebView.onEvent('write_access_requested', onWriteAccessRequested); 1702 | WebView.onEvent('phone_requested', onPhoneRequested); 1703 | WebView.onEvent('custom_method_invoked', onCustomMethodInvoked); 1704 | WebView.postEvent('web_app_request_theme'); 1705 | WebView.postEvent('web_app_request_viewport'); 1706 | 1707 | })(); 1708 | --------------------------------------------------------------------------------