From cd361a1eaa122b4b7da387f3e20db75cd11c9beb Mon Sep 17 00:00:00 2001 From: martbost Date: Sun, 23 Aug 2026 05:56:51 -0500 Subject: [PATCH] Telegram Mini App v1: initData auth bridge into the existing site - POST /api/public/tg-webapp-auth: HMAC-verifies WebApp initData against the companion bot token (12h freshness, timing-safe), maps chat -> member via tg-links.json, mints a message session -> linked members land on /my/ with zero login - /app entry page (vendored telegram-web-app.js keeps CSP script-src 'self'); unlinked users get the one-time wallet-link instructions - tg-app.js on all pages: no-op in browsers; inside the webview lazy-loads the SDK, expands, themes header/background #071421, wires native BackButton - Bot menu button set programmatically to open /app; /start + help mention it - Synced chat.js canned answer + AI system prompt (Mini App facts) Co-Authored-By: Claude Fable 5 --- messages.js | 12 +- public/app.html | 30 + public/app.js | 37 + public/chat.js | 4 +- public/contract.html | 2 +- public/disclaimer.html | 2 +- public/fast-start.html | 2 +- public/how-pay-works.html | 2 +- public/index.html | 2 +- public/join.html | 2 +- public/my.html | 2 +- public/start.html | 2 +- public/telegram-web-app.js | 3397 ++++++++++++++++++++++++++++++++++++ public/tg-app.js | 32 + public/tools.html | 2 +- public/training.html | 2 +- public/weekly-rhythm.html | 2 +- server.js | 21 +- tgbot.js | 31 +- 19 files changed, 3567 insertions(+), 19 deletions(-) create mode 100644 public/app.html create mode 100644 public/app.js create mode 100644 public/telegram-web-app.js create mode 100644 public/tg-app.js diff --git a/messages.js b/messages.js index 9ab37d1..b0f42a2 100644 --- a/messages.js +++ b/messages.js @@ -89,6 +89,16 @@ function verifyChallenge(address, signature) { saveSessions(); return { token, id }; } +// Mini App sessions: identity was already proved once (wallet-verified +// Telegram link), and Telegram re-proves the chat via signed initData — so a +// session can be minted without a fresh wallet signature. No address attached. +function mintSession(id) { + if (!Number.isInteger(id) || id < 1) return null; + const token = crypto.randomBytes(32).toString('hex'); + sessions.set(token, { address: null, id, expires: Date.now() + SESSION_TTL, via: 'tg' }); + saveSessions(); + return token; +} function authFromCookie(req) { const m = /(?:^|;\s*)ctb\.msid=([^;]+)/.exec(req.headers.cookie || ''); if (!m) return null; @@ -168,4 +178,4 @@ function adminList() { return getMessages().slice(-300).reverse().map(m => ({ mid: m.mid, fromId: m.fromId, toId: m.toId || null, org: !!m.org, body: m.body, ts: m.ts, readCount: Object.keys(m.read || {}).length })); } -module.exports = { init, makeChallenge, verifyChallenge, authFromCookie, sessionCookie, send, inbox, markRead, unreadCount, adminList, ADDR_RE }; +module.exports = { init, makeChallenge, verifyChallenge, mintSession, authFromCookie, sessionCookie, send, inbox, markRead, unreadCount, adminList, ADDR_RE }; diff --git a/public/app.html b/public/app.html new file mode 100644 index 0000000..b3eea35 --- /dev/null +++ b/public/app.html @@ -0,0 +1,30 @@ +RM Circle + + + +
+
+ RM Circle +

Opening your dashboard…

+

Verifying your Telegram link.

+
+ + + +
+ diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..6ba5416 --- /dev/null +++ b/public/app.js @@ -0,0 +1,37 @@ +// Telegram Mini App entry: verify initData server-side, then land the linked +// member on THEIR dashboard with a minted session — zero login. Unlinked +// users get the one-time wallet-link instructions instead. +(function () { + 'use strict'; + var tg = window.Telegram && window.Telegram.WebApp; + function show(id) { + ['st-loading', 'st-unlinked', 'st-error', 'st-notg'].forEach(function (x) { + var el = document.getElementById(x); if (el) el.style.display = x === id ? '' : 'none'; + }); + } + function on(id, fn) { var el = document.getElementById(id); if (el) el.addEventListener('click', fn); } + on('btn-open-site', function () { + var url = location.origin + '/my'; + if (tg && tg.openLink) tg.openLink(url); else location.href = url; + }); + on('btn-browse', function () { location.href = '/start'; }); + on('btn-retry', function () { location.reload(); }); + + if (!tg || !tg.initData) { show('st-notg'); return; } + // Flag the webview session so tg-app.js activates on every page after this one. + try { sessionStorage.setItem('rmcTg', '1'); } catch (e) {} + tg.ready(); + try { tg.expand(); } catch (e) {} + try { tg.setHeaderColor('#071421'); tg.setBackgroundColor('#071421'); } catch (e) {} + + fetch('/api/public/tg-webapp-auth', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ initData: tg.initData }) + }).then(function (r) { return r.json(); }).then(function (d) { + if (d && d.ok && d.linked) { location.replace('/my/' + d.id); return; } + if (d && d.ok) { show('st-unlinked'); return; } + var el = document.getElementById('err-detail'); + if (el && d && d.error) el.textContent = d.error; + show('st-error'); + }).catch(function () { show('st-error'); }); +})(); diff --git a/public/chat.js b/public/chat.js index d89c9b4..938546c 100644 --- a/public/chat.js +++ b/public/chat.js @@ -35,8 +35,8 @@ a:()=>`Great question — and we checked it on-chain, not just in theory. The contract needs no one to keep it running: joins, upgrades, placement and every payout are fully automatic — no button anyone has to press, no expiry. If the creators walked away, lost their keys, or vanished, member payments keep flowing exactly as coded. We also verified that the founder, development and fee wallets are ordinary wallets, not programs — an ordinary wallet always accepts an incoming payment even if its key is lost forever, so a dead admin wallet can't jam a single member payment (at worst the project's own fee sits there uncollected). And the contract holds no stored balance — every payment is delivered in the same transaction. Full write-up in section 6 of rmcircle.team/contract.`}, {k:['pyramid','ponzi','pyramid scheme','ponzi scheme','mlm','recruiting scheme','is this a scheme'], a:()=>`A pyramid or Ponzi scheme funnels everyone's money to a central company and pays earlier joiners out of later joiners' deposits — and you can't verify any of it. This is the opposite: no company holds the money. A public smart contract on Polygon sends each payment person-to-person in the same transaction it arrives, and you can read the code and every payout yourself on-chain — nothing pooled, nothing hidden, rules that can't be changed. It is a team build, so it takes real effort and carries real crypto risk — not a passive investment. But you don't have to trust anyone; verify it at rmcircle.team/contract. No income is guaranteed.`}, - {k:['telegram bot','connect telegram','payout ping','telegram notification','message my downline','contact my downline','reach my downline'], - a:()=>`Link your position to our Telegram companion bot: open your dashboard → Messages → sign in with your wallet → tap "Connect Telegram". Once linked you get an instant DM whenever your position catches a payment, team messages reach you natively in Telegram (reply right there to answer), you're pinged when someone joins on your link, and "links" gives you all your invite links. Messaging still follows your matrix lines only — same rules as the site.`}, + {k:['telegram bot','connect telegram','payout ping','telegram notification','message my downline','contact my downline','reach my downline','mini app','miniapp','telegram app','dashboard in telegram'], + a:()=>`Link your position to our Telegram companion bot: open your dashboard → Messages → sign in with your wallet → tap "Connect Telegram". Once linked you get an instant DM whenever your position catches a payment, team messages reach you natively in Telegram (reply right there to answer), you're pinged when someone joins on your link, and "links" gives you all your invite links. Linked members can also tap the bot's ☰ menu button to open the Mini App — your full live dashboard, promo tools, and the Circle Method right inside Telegram, no login needed. Messaging still follows your matrix lines only — same rules as the site.`}, {k:['cash out','cashout','spend my crypto','withdraw','off ramp','off-ramp','gift card','giftcard','get my money out','turn into cash','convert to dollars'], a:()=>`Three good paths, easiest first: (1) E-gift cards — send POL to CWallet (cwallet.com), swap to a US-dollar token there (their internal swaps are virtually free), and buy gift cards for brands you already use — groceries, gas, Amazon. eGifter (egifter.com) also takes crypto directly. (2) Straight cash-out via a regulated exchange in your country (Coinbase, Kraken…): send, sell, withdraw to your bank. (3) Keep it working — many members leave catches in the wallet to fund their next level. Full guide: Spending what you earn. Honest notes: those are independent custodial services — only move what you're about to spend; taxes may apply where you live; not financial advice.`}, {k:['circle method','recruiting course','how do i recruit','recruiting training','get my two','get your two','how to invite','module 1','lessons'], diff --git a/public/contract.html b/public/contract.html index a65d51f..8e709d0 100644 --- a/public/contract.html +++ b/public/contract.html @@ -30,4 +30,4 @@ - + diff --git a/public/disclaimer.html b/public/disclaimer.html index e0192ed..8d0a13c 100644 --- a/public/disclaimer.html +++ b/public/disclaimer.html @@ -25,4 +25,4 @@ - + diff --git a/public/fast-start.html b/public/fast-start.html index 06a8ef3..bc4b74f 100644 --- a/public/fast-start.html +++ b/public/fast-start.html @@ -111,4 +111,4 @@ - + diff --git a/public/how-pay-works.html b/public/how-pay-works.html index 5141c61..f7a8a66 100644 --- a/public/how-pay-works.html +++ b/public/how-pay-works.html @@ -74,4 +74,4 @@ - + diff --git a/public/index.html b/public/index.html index d6a3be8..4fc4f49 100644 --- a/public/index.html +++ b/public/index.html @@ -23,4 +23,4 @@
Ready to start?

See the current team placement.

The onboarding page automatically shows the sponsor position the team is currently helping. Always use the sponsor shown there instead of an old screenshot or saved link.

Open Getting Started Instructions →
- + diff --git a/public/join.html b/public/join.html index 1142fe8..49c6bf5 100644 --- a/public/join.html +++ b/public/join.html @@ -29,4 +29,4 @@
Risk reminder: participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.
This independent team page is educational and is not an earnings guarantee or investment advice. Cryptocurrency and smart-contract participation involve risk, including possible loss of funds. Never use funds you cannot afford to lose. Results depend on actual participation, qualification, upgrades, smart-contract behavior, and the market value of POL.
- + diff --git a/public/my.html b/public/my.html index 2e504b4..f018f9c 100644 --- a/public/my.html +++ b/public/my.html @@ -31,4 +31,4 @@
All figures are read live from the RM Circle smart contract on Polygon and are historical facts, not a promise of future results. Participation involves cryptocurrency and smart-contract risk. Never use funds you cannot afford to lose.
- + diff --git a/public/start.html b/public/start.html index de6d48d..f791d1f 100644 --- a/public/start.html +++ b/public/start.html @@ -16,4 +16,4 @@
Risk reminder: participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.
RM Circle Premium Team Build Roadmap — core strategy, step-by-step guide, premium levels, and duplication formula
This roadmap is the plan every member follows — tap to view full size.
- + diff --git a/public/telegram-web-app.js b/public/telegram-web-app.js new file mode 100644 index 0000000..d84a5ea --- /dev/null +++ b/public/telegram-web-app.js @@ -0,0 +1,3397 @@ +// WebView +(function () { + var eventHandlers = {}; + + var locationHash = ''; + try { + locationHash = location.hash.toString(); + } catch (e) {} + + var initParams = urlParseHashParams(locationHash); + var storedParams = sessionStorageGet('initParams'); + if (storedParams) { + for (var key in storedParams) { + if (typeof initParams[key] === 'undefined') { + initParams[key] = storedParams[key]; + } + } + } + sessionStorageSet('initParams', initParams); + + var isIframe = false, iFrameStyle; + try { + isIframe = (window.parent != null && window != window.parent); + if (isIframe) { + window.addEventListener('message', function (event) { + if (event.source !== window.parent) return; + try { + var dataParsed = JSON.parse(event.data); + } catch (e) { + return; + } + if (!dataParsed || !dataParsed.eventType) { + return; + } + if (dataParsed.eventType == 'set_custom_style') { + if (event.origin === 'https://web.telegram.org') { + iFrameStyle.innerHTML = dataParsed.eventData; + } + } else if (dataParsed.eventType == 'reload_iframe') { + try { + window.parent.postMessage(JSON.stringify({eventType: 'iframe_will_reload'}), '*'); + } catch (e) {} + location.reload(); + } else { + receiveEvent(dataParsed.eventType, dataParsed.eventData); + } + }); + iFrameStyle = document.createElement('style'); + document.head.appendChild(iFrameStyle); + try { + window.parent.postMessage(JSON.stringify({eventType: 'iframe_ready', eventData: {reload_supported: true}}), '*'); + } catch (e) {} + } + } catch (e) {} + + function urlSafeDecode(urlencoded) { + try { + urlencoded = urlencoded.replace(/\+/g, '%20'); + return decodeURIComponent(urlencoded); + } catch (e) { + return urlencoded; + } + } + + function urlParseHashParams(locationHash) { + locationHash = locationHash.replace(/^#/, ''); + var params = {}; + if (!locationHash.length) { + return params; + } + if (locationHash.indexOf('=') < 0 && locationHash.indexOf('?') < 0) { + params._path = urlSafeDecode(locationHash); + return params; + } + var qIndex = locationHash.indexOf('?'); + if (qIndex >= 0) { + var pathParam = locationHash.substr(0, qIndex); + params._path = urlSafeDecode(pathParam); + locationHash = locationHash.substr(qIndex + 1); + } + var query_params = urlParseQueryString(locationHash); + for (var k in query_params) { + params[k] = query_params[k]; + } + return params; + } + + function urlParseQueryString(queryString) { + var params = {}; + if (!queryString.length) { + return params; + } + var queryStringParams = queryString.split('&'); + var i, param, paramName, paramValue; + for (i = 0; i < queryStringParams.length; i++) { + param = queryStringParams[i].split('='); + paramName = urlSafeDecode(param[0]); + paramValue = param[1] == null ? null : urlSafeDecode(param[1]); + params[paramName] = paramValue; + } + return params; + } + + // Telegram apps will implement this logic to add service params (e.g. tgShareScoreUrl) to game URL + function urlAppendHashParams(url, addHash) { + // url looks like 'https://game.com/path?query=1#hash' + // addHash looks like 'tgShareScoreUrl=' + encodeURIComponent('tgb://share_game_score?hash=very_long_hash123') + + var ind = url.indexOf('#'); + if (ind < 0) { + // https://game.com/path -> https://game.com/path#tgShareScoreUrl=etc + return url + '#' + addHash; + } + var curHash = url.substr(ind + 1); + if (curHash.indexOf('=') >= 0 || curHash.indexOf('?') >= 0) { + // https://game.com/#hash=1 -> https://game.com/#hash=1&tgShareScoreUrl=etc + // https://game.com/#path?query -> https://game.com/#path?query&tgShareScoreUrl=etc + return url + '&' + addHash; + } + // https://game.com/#hash -> https://game.com/#hash?tgShareScoreUrl=etc + if (curHash.length > 0) { + return url + '?' + addHash; + } + // https://game.com/# -> https://game.com/#tgShareScoreUrl=etc + return url + addHash; + } + + function postEvent(eventType, callback, eventData) { + if (!callback) { + callback = function () {}; + } + if (eventData === undefined) { + eventData = ''; + } + console.log('[Telegram.WebView] > postEvent', eventType, eventData); + + if (window.TelegramWebviewProxy !== undefined) { + TelegramWebviewProxy.postEvent(eventType, JSON.stringify(eventData)); + callback(); + } + else if (window.external && 'notify' in window.external) { + window.external.notify(JSON.stringify({eventType: eventType, eventData: eventData})); + callback(); + } + else if (isIframe) { + try { + var trustedTarget = 'https://web.telegram.org'; + // For now we don't restrict target, for testing purposes + trustedTarget = '*'; + window.parent.postMessage(JSON.stringify({eventType: eventType, eventData: eventData}), trustedTarget); + callback(); + } catch (e) { + callback(e); + } + } + else { + callback({notAvailable: true}); + } + }; + + function receiveEvent(eventType, eventData) { + console.log('[Telegram.WebView] < receiveEvent', eventType, eventData); + callEventCallbacks(eventType, function(callback) { + callback(eventType, eventData); + }); + } + + function callEventCallbacks(eventType, func) { + var curEventHandlers = eventHandlers[eventType]; + if (curEventHandlers === undefined || + !curEventHandlers.length) { + return; + } + for (var i = 0; i < curEventHandlers.length; i++) { + try { + func(curEventHandlers[i]); + } catch (e) {} + } + } + + function onEvent(eventType, callback) { + if (eventHandlers[eventType] === undefined) { + eventHandlers[eventType] = []; + } + var index = eventHandlers[eventType].indexOf(callback); + if (index === -1) { + eventHandlers[eventType].push(callback); + } + }; + + function offEvent(eventType, callback) { + if (eventHandlers[eventType] === undefined) { + return; + } + var index = eventHandlers[eventType].indexOf(callback); + if (index === -1) { + return; + } + eventHandlers[eventType].splice(index, 1); + }; + + function openProtoUrl(url) { + if (!url.match(/^(web\+)?tgb?:\/\/./)) { + return false; + } + var useIframe = navigator.userAgent.match(/iOS|iPhone OS|iPhone|iPod|iPad/i) ? true : false; + if (useIframe) { + var iframeContEl = document.getElementById('tgme_frame_cont') || document.body; + var iframeEl = document.createElement('iframe'); + iframeContEl.appendChild(iframeEl); + var pageHidden = false; + var enableHidden = function () { + pageHidden = true; + }; + window.addEventListener('pagehide', enableHidden, false); + window.addEventListener('blur', enableHidden, false); + if (iframeEl !== null) { + iframeEl.src = url; + } + setTimeout(function() { + if (!pageHidden) { + window.location = url; + } + window.removeEventListener('pagehide', enableHidden, false); + window.removeEventListener('blur', enableHidden, false); + }, 2000); + } + else { + window.location = url; + } + return true; + } + + function sessionStorageSet(key, value) { + try { + window.sessionStorage.setItem('__telegram__' + key, JSON.stringify(value)); + return true; + } catch(e) {} + return false; + } + function sessionStorageGet(key) { + try { + return JSON.parse(window.sessionStorage.getItem('__telegram__' + key)); + } catch(e) {} + return null; + } + + if (!window.Telegram) { + window.Telegram = {}; + } + window.Telegram.WebView = { + initParams: initParams, + isIframe: isIframe, + onEvent: onEvent, + offEvent: offEvent, + postEvent: postEvent, + receiveEvent: receiveEvent, + callEventCallbacks: callEventCallbacks + }; + + window.Telegram.Utils = { + urlSafeDecode: urlSafeDecode, + urlParseQueryString: urlParseQueryString, + urlParseHashParams: urlParseHashParams, + urlAppendHashParams: urlAppendHashParams, + sessionStorageSet: sessionStorageSet, + sessionStorageGet: sessionStorageGet + }; + + // For Windows Phone app + window.TelegramGameProxy_receiveEvent = receiveEvent; + + // App backward compatibility + window.TelegramGameProxy = { + receiveEvent: receiveEvent + }; +})(); + +// WebApp +(function () { + var Utils = window.Telegram.Utils; + var WebView = window.Telegram.WebView; + var initParams = WebView.initParams; + var isIframe = WebView.isIframe; + + var WebApp = {}; + var webAppInitData = '', webAppInitDataUnsafe = {}; + var themeParams = {}, colorScheme = 'light'; + var webAppVersion = '6.0'; + var webAppPlatform = 'unknown'; + var webAppIsActive = true; + var webAppIsFullscreen = false; + var webAppIsOrientationLocked = false; + var webAppBackgroundColor = 'bg_color'; + var webAppHeaderColorKey = 'bg_color'; + var webAppHeaderColor = null; + + if (initParams.tgWebAppData && initParams.tgWebAppData.length) { + webAppInitData = initParams.tgWebAppData; + webAppInitDataUnsafe = Utils.urlParseQueryString(webAppInitData); + for (var key in webAppInitDataUnsafe) { + var val = webAppInitDataUnsafe[key]; + try { + if (val.substr(0, 1) == '{' && val.substr(-1) == '}' || + val.substr(0, 1) == '[' && val.substr(-1) == ']') { + webAppInitDataUnsafe[key] = JSON.parse(val); + } + } catch (e) {} + } + } + var stored_theme_params = Utils.sessionStorageGet('themeParams'); + if (initParams.tgWebAppThemeParams && initParams.tgWebAppThemeParams.length) { + var themeParamsRaw = initParams.tgWebAppThemeParams; + try { + var theme_params = JSON.parse(themeParamsRaw); + if (theme_params) { + setThemeParams(theme_params); + } + } catch (e) {} + } + if (stored_theme_params) { + setThemeParams(stored_theme_params); + } + var stored_def_colors = Utils.sessionStorageGet('defaultColors'); + if (initParams.tgWebAppDefaultColors && initParams.tgWebAppDefaultColors.length) { + var defColorsRaw = initParams.tgWebAppDefaultColors; + try { + var def_colors = JSON.parse(defColorsRaw); + if (def_colors) { + setDefaultColors(def_colors); + } + } catch (e) {} + } + if (stored_def_colors) { + setDefaultColors(stored_def_colors); + } + if (initParams.tgWebAppVersion) { + webAppVersion = initParams.tgWebAppVersion; + } + if (initParams.tgWebAppPlatform) { + webAppPlatform = initParams.tgWebAppPlatform; + } + + var stored_fullscreen = Utils.sessionStorageGet('isFullscreen'); + if (initParams.tgWebAppFullscreen) { + setFullscreen(true); + } + if (stored_fullscreen) { + setFullscreen(stored_fullscreen == 'yes'); + } + + var stored_orientation_lock = Utils.sessionStorageGet('isOrientationLocked'); + if (stored_orientation_lock) { + setOrientationLock(stored_orientation_lock == 'yes'); + } + + function onThemeChanged(eventType, eventData) { + if (eventData.theme_params) { + setThemeParams(eventData.theme_params); + window.Telegram.WebApp.MainButton.setParams({}); + window.Telegram.WebApp.SecondaryButton.setParams({}); + updateHeaderColor(); + updateBackgroundColor(); + updateBottomBarColor(); + receiveWebViewEvent('themeChanged'); + } + } + + var lastWindowHeight = window.innerHeight; + function onViewportChanged(eventType, eventData) { + if (eventData.height) { + window.removeEventListener('resize', onWindowResize); + setViewportHeight(eventData); + } + } + + function onWindowResize(e) { + if (lastWindowHeight != window.innerHeight) { + lastWindowHeight = window.innerHeight; + receiveWebViewEvent('viewportChanged', { + isStateStable: true + }); + } + } + + function onSafeAreaChanged(eventType, eventData) { + if (eventData) { + setSafeAreaInset(eventData); + } + } + function onContentSafeAreaChanged(eventType, eventData) { + if (eventData) { + setContentSafeAreaInset(eventData); + } + } + + function onVisibilityChanged(eventType, eventData) { + if (eventData.is_visible) { + webAppIsActive = true; + receiveWebViewEvent('activated'); + } else { + webAppIsActive = false; + receiveWebViewEvent('deactivated'); + } + } + + function linkHandler(e) { + if (e.metaKey || e.ctrlKey) return; + var el = e.target; + while (el.tagName != 'A' && el.parentNode) { + el = el.parentNode; + } + if (el.tagName == 'A' && + el.target != '_blank' && + (el.protocol == 'http:' || el.protocol == 'https:') && + isTmeHostname(el.hostname)) { + WebApp.openTelegramLink(el.href); + e.preventDefault(); + } + } + + function strTrim(str) { + return str.toString().replace(/^\s+|\s+$/g, ''); + } + + function isTmeHostname(hostname) { + hostname = hostname.toString().toLowerCase(); + return hostname == 't.me' || hostname == 'telegram.me'; + } + + function receiveWebViewEvent(eventType) { + var args = Array.prototype.slice.call(arguments); + eventType = args.shift(); + WebView.callEventCallbacks('webview:' + eventType, function(callback) { + callback.apply(WebApp, args); + }); + } + + function onWebViewEvent(eventType, callback) { + WebView.onEvent('webview:' + eventType, callback); + }; + + function offWebViewEvent(eventType, callback) { + WebView.offEvent('webview:' + eventType, callback); + }; + + function setCssProperty(name, value) { + var root = document.documentElement; + if (root && root.style && root.style.setProperty) { + root.style.setProperty('--tg-' + name, value); + } + } + + function setFullscreen(is_fullscreen) { + webAppIsFullscreen = !!is_fullscreen; + Utils.sessionStorageSet('isFullscreen', webAppIsFullscreen ? 'yes' : 'no'); + } + + function setOrientationLock(is_locked) { + webAppIsOrientationLocked = !!is_locked; + Utils.sessionStorageSet('isOrientationLocked', webAppIsOrientationLocked ? 'yes' : 'no'); + } + + function setThemeParams(theme_params) { + // temp iOS fix + if (theme_params.bg_color == '#1c1c1d' && + theme_params.bg_color == theme_params.secondary_bg_color) { + theme_params.secondary_bg_color = '#2c2c2e'; + } + var color; + for (var key in theme_params) { + if (color = parseColorToHex(theme_params[key])) { + themeParams[key] = color; + if (key == 'bg_color') { + colorScheme = isColorDark(color) ? 'dark' : 'light' + setCssProperty('color-scheme', colorScheme); + } + key = 'theme-' + key.split('_').join('-'); + setCssProperty(key, color); + } + } + Utils.sessionStorageSet('themeParams', themeParams); + } + + function setDefaultColors(def_colors) { + if (colorScheme == 'dark') { + if (def_colors.bg_dark_color) { + webAppBackgroundColor = def_colors.bg_dark_color; + } + if (def_colors.header_dark_color) { + webAppHeaderColorKey = null; + webAppHeaderColor = def_colors.header_dark_color; + } + } else { + if (def_colors.bg_color) { + webAppBackgroundColor = def_colors.bg_color; + } + if (def_colors.header_color) { + webAppHeaderColorKey = null; + webAppHeaderColor = def_colors.header_color; + } + } + Utils.sessionStorageSet('defaultColors', def_colors); + } + + var webAppCallbacks = {}; + function generateCallbackId(len) { + var tries = 100; + while (--tries) { + var id = '', chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', chars_len = chars.length; + for (var i = 0; i < len; i++) { + id += chars[Math.floor(Math.random() * chars_len)]; + } + if (!webAppCallbacks[id]) { + webAppCallbacks[id] = {}; + return id; + } + } + throw Error('WebAppCallbackIdGenerateFailed'); + } + + var viewportHeight = false, viewportStableHeight = false, isExpanded = true; + function setViewportHeight(data) { + if (typeof data !== 'undefined') { + isExpanded = !!data.is_expanded; + viewportHeight = data.height; + if (data.is_state_stable) { + viewportStableHeight = data.height; + } + receiveWebViewEvent('viewportChanged', { + isStateStable: !!data.is_state_stable + }); + } + var height, stable_height; + if (viewportHeight !== false) { + height = (viewportHeight - bottomBarHeight) + 'px'; + } else { + height = bottomBarHeight ? 'calc(100vh - ' + bottomBarHeight + 'px)' : '100vh'; + } + if (viewportStableHeight !== false) { + stable_height = (viewportStableHeight - bottomBarHeight) + 'px'; + } else { + stable_height = bottomBarHeight ? 'calc(100vh - ' + bottomBarHeight + 'px)' : '100vh'; + } + setCssProperty('viewport-height', height); + setCssProperty('viewport-stable-height', stable_height); + } + + var safeAreaInset = {top: 0, bottom: 0, left: 0, right: 0}; + function setSafeAreaInset(data) { + if (typeof data !== 'undefined') { + if (typeof data.top !== 'undefined') { + safeAreaInset.top = data.top; + } + if (typeof data.bottom !== 'undefined') { + safeAreaInset.bottom = data.bottom; + } + if (typeof data.left !== 'undefined') { + safeAreaInset.left = data.left; + } + if (typeof data.right !== 'undefined') { + safeAreaInset.right = data.right; + } + receiveWebViewEvent('safeAreaChanged'); + } + setCssProperty('safe-area-inset-top', safeAreaInset.top + 'px'); + setCssProperty('safe-area-inset-bottom', safeAreaInset.bottom + 'px'); + setCssProperty('safe-area-inset-left', safeAreaInset.left + 'px'); + setCssProperty('safe-area-inset-right', safeAreaInset.right + 'px'); + } + + var contentSafeAreaInset = {top: 0, bottom: 0, left: 0, right: 0}; + function setContentSafeAreaInset(data) { + if (typeof data !== 'undefined') { + if (typeof data.top !== 'undefined') { + contentSafeAreaInset.top = data.top; + } + if (typeof data.bottom !== 'undefined') { + contentSafeAreaInset.bottom = data.bottom; + } + if (typeof data.left !== 'undefined') { + contentSafeAreaInset.left = data.left; + } + if (typeof data.right !== 'undefined') { + contentSafeAreaInset.right = data.right; + } + receiveWebViewEvent('contentSafeAreaChanged'); + } + setCssProperty('content-safe-area-inset-top', contentSafeAreaInset.top + 'px'); + setCssProperty('content-safe-area-inset-bottom', contentSafeAreaInset.bottom + 'px'); + setCssProperty('content-safe-area-inset-left', contentSafeAreaInset.left + 'px'); + setCssProperty('content-safe-area-inset-right', contentSafeAreaInset.right + 'px'); + } + + var isClosingConfirmationEnabled = false; + function setClosingConfirmation(need_confirmation) { + if (!versionAtLeast('6.2')) { + console.warn('[Telegram.WebApp] Closing confirmation is not supported in version ' + webAppVersion); + return; + } + isClosingConfirmationEnabled = !!need_confirmation; + WebView.postEvent('web_app_setup_closing_behavior', false, {need_confirmation: isClosingConfirmationEnabled}); + } + + var isVerticalSwipesEnabled = true; + function toggleVerticalSwipes(enable_swipes) { + if (!versionAtLeast('7.7')) { + console.warn('[Telegram.WebApp] Changing swipes behavior is not supported in version ' + webAppVersion); + return; + } + isVerticalSwipesEnabled = !!enable_swipes; + WebView.postEvent('web_app_setup_swipe_behavior', false, {allow_vertical_swipe: isVerticalSwipesEnabled}); + } + + function onFullscreenChanged(eventType, eventData) { + setFullscreen(eventData.is_fullscreen); + receiveWebViewEvent('fullscreenChanged'); + } + function onFullscreenFailed(eventType, eventData) { + if (eventData.error == 'ALREADY_FULLSCREEN' && !webAppIsFullscreen) { + setFullscreen(true); + } + receiveWebViewEvent('fullscreenFailed', { + error: eventData.error + }); + } + + function toggleOrientationLock(locked) { + if (!versionAtLeast('8.0')) { + console.warn('[Telegram.WebApp] Orientation locking is not supported in version ' + webAppVersion); + return; + } + setOrientationLock(locked); + WebView.postEvent('web_app_toggle_orientation_lock', false, {locked: webAppIsOrientationLocked}); + } + + var homeScreenCallbacks = []; + function onHomeScreenAdded(eventType, eventData) { + receiveWebViewEvent('homeScreenAdded'); + } + function onHomeScreenChecked(eventType, eventData) { + var status = eventData.status || 'unknown'; + if (homeScreenCallbacks.length > 0) { + for (var i = 0; i < homeScreenCallbacks.length; i++) { + var callback = homeScreenCallbacks[i]; + callback(status); + } + homeScreenCallbacks = []; + } + receiveWebViewEvent('homeScreenChecked', { + status: status + }); + } + + var WebAppShareMessageOpened = false; + function onPreparedMessageSent(eventType, eventData) { + if (WebAppShareMessageOpened) { + var requestData = WebAppShareMessageOpened; + WebAppShareMessageOpened = false; + if (requestData.callback) { + requestData.callback(true); + } + receiveWebViewEvent('shareMessageSent'); + } + } + function onPreparedMessageFailed(eventType, eventData) { + if (WebAppShareMessageOpened) { + var requestData = WebAppShareMessageOpened; + WebAppShareMessageOpened = false; + if (requestData.callback) { + requestData.callback(false); + } + receiveWebViewEvent('shareMessageFailed', { + error: eventData.error + }); + } + } + + var WebAppRequestChatOpened = false; + function onRequestedChatSent(eventType, eventData) { + if (WebAppRequestChatOpened) { + var requestData = WebAppRequestChatOpened; + WebAppRequestChatOpened = false; + if (requestData.callback) { + requestData.callback(true); + } + receiveWebViewEvent('requestedChatSent'); + } + } + function onRequestedChatFailed(eventType, eventData) { + if (WebAppRequestChatOpened) { + var requestData = WebAppRequestChatOpened; + WebAppRequestChatOpened = false; + if (requestData.callback) { + requestData.callback(false); + } + receiveWebViewEvent('requestedChatFailed', { + error: eventData.error + }); + } + } + + var WebAppEmojiStatusRequested = false; + function onEmojiStatusSet(eventType, eventData) { + if (WebAppEmojiStatusRequested) { + var requestData = WebAppEmojiStatusRequested; + WebAppEmojiStatusRequested = false; + if (requestData.callback) { + requestData.callback(true); + } + receiveWebViewEvent('emojiStatusSet'); + } + } + function onEmojiStatusFailed(eventType, eventData) { + if (WebAppEmojiStatusRequested) { + var requestData = WebAppEmojiStatusRequested; + WebAppEmojiStatusRequested = false; + if (requestData.callback) { + requestData.callback(false); + } + receiveWebViewEvent('emojiStatusFailed', { + error: eventData.error + }); + } + } + var WebAppEmojiStatusAccessRequested = false; + function onEmojiStatusAccessRequested(eventType, eventData) { + if (WebAppEmojiStatusAccessRequested) { + var requestData = WebAppEmojiStatusAccessRequested; + WebAppEmojiStatusAccessRequested = false; + if (requestData.callback) { + requestData.callback(eventData.status == 'allowed'); + } + receiveWebViewEvent('emojiStatusAccessRequested', { + status: eventData.status + }); + } + } + + var webAppPopupOpened = false; + function onPopupClosed(eventType, eventData) { + if (webAppPopupOpened) { + var popupData = webAppPopupOpened; + webAppPopupOpened = false; + var button_id = null; + if (typeof eventData.button_id !== 'undefined') { + button_id = eventData.button_id; + } + if (popupData.callback) { + popupData.callback(button_id); + } + receiveWebViewEvent('popupClosed', { + button_id: button_id + }); + } + } + + + function getHeaderColor() { + if (webAppHeaderColorKey == 'secondary_bg_color') { + return themeParams.secondary_bg_color; + } else if (webAppHeaderColorKey == 'bg_color') { + return themeParams.bg_color; + } + return webAppHeaderColor; + } + function setHeaderColor(color) { + if (!versionAtLeast('6.1')) { + console.warn('[Telegram.WebApp] Header color is not supported in version ' + webAppVersion); + return; + } + if (!versionAtLeast('6.9')) { + if (themeParams.bg_color && + themeParams.bg_color == color) { + color = 'bg_color'; + } else if (themeParams.secondary_bg_color && + themeParams.secondary_bg_color == color) { + color = 'secondary_bg_color'; + } + } + var head_color = null, color_key = null; + if (color == 'bg_color' || color == 'secondary_bg_color') { + color_key = color; + } else if (versionAtLeast('6.9')) { + head_color = parseColorToHex(color); + if (!head_color) { + console.error('[Telegram.WebApp] Header color format is invalid', color); + throw Error('WebAppHeaderColorInvalid'); + } + } + if (!versionAtLeast('6.9') && + color_key != 'bg_color' && + color_key != 'secondary_bg_color') { + 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); + throw Error('WebAppHeaderColorKeyInvalid'); + } + webAppHeaderColorKey = color_key; + webAppHeaderColor = head_color; + updateHeaderColor(); + } + var appHeaderColorKey = null, appHeaderColor = null; + function updateHeaderColor() { + if (appHeaderColorKey != webAppHeaderColorKey || + appHeaderColor != webAppHeaderColor) { + appHeaderColorKey = webAppHeaderColorKey; + appHeaderColor = webAppHeaderColor; + if (appHeaderColor) { + WebView.postEvent('web_app_set_header_color', false, {color: webAppHeaderColor}); + } else { + WebView.postEvent('web_app_set_header_color', false, {color_key: webAppHeaderColorKey}); + } + } + } + + function getBackgroundColor() { + if (webAppBackgroundColor == 'secondary_bg_color') { + return themeParams.secondary_bg_color; + } else if (webAppBackgroundColor == 'bg_color') { + return themeParams.bg_color; + } + return webAppBackgroundColor; + } + function setBackgroundColor(color) { + if (!versionAtLeast('6.1')) { + console.warn('[Telegram.WebApp] Background color is not supported in version ' + webAppVersion); + return; + } + var bg_color; + if (color == 'bg_color' || color == 'secondary_bg_color') { + bg_color = color; + } else { + bg_color = parseColorToHex(color); + if (!bg_color) { + console.error('[Telegram.WebApp] Background color format is invalid', color); + throw Error('WebAppBackgroundColorInvalid'); + } + } + webAppBackgroundColor = bg_color; + updateBackgroundColor(); + } + var appBackgroundColor = null; + function updateBackgroundColor() { + var color = getBackgroundColor(); + if (appBackgroundColor != color) { + appBackgroundColor = color; + WebView.postEvent('web_app_set_background_color', false, {color: color}); + } + } + + var bottomBarColor = 'bottom_bar_bg_color'; + function getBottomBarColor() { + if (bottomBarColor == 'bottom_bar_bg_color') { + return themeParams.bottom_bar_bg_color || themeParams.secondary_bg_color || '#ffffff'; + } else if (bottomBarColor == 'secondary_bg_color') { + return themeParams.secondary_bg_color; + } else if (bottomBarColor == 'bg_color') { + return themeParams.bg_color; + } + return bottomBarColor; + } + function setBottomBarColor(color) { + if (!versionAtLeast('7.10')) { + console.warn('[Telegram.WebApp] Bottom bar color is not supported in version ' + webAppVersion); + return; + } + var bg_color; + if (color == 'bg_color' || color == 'secondary_bg_color' || color == 'bottom_bar_bg_color') { + bg_color = color; + } else { + bg_color = parseColorToHex(color); + if (!bg_color) { + console.error('[Telegram.WebApp] Bottom bar color format is invalid', color); + throw Error('WebAppBottomBarColorInvalid'); + } + } + bottomBarColor = bg_color; + updateBottomBarColor(); + window.Telegram.WebApp.SecondaryButton.setParams({}); + } + var appBottomBarColor = null; + function updateBottomBarColor() { + var color = getBottomBarColor(); + if (appBottomBarColor != color) { + appBottomBarColor = color; + WebView.postEvent('web_app_set_bottom_bar_color', false, {color: color}); + } + if (initParams.tgWebAppDebug) { + updateDebugBottomBar(); + } + } + + + function parseColorToHex(color) { + color += ''; + var match; + if (match = /^\s*#([0-9a-f]{6})\s*$/i.exec(color)) { + return '#' + match[1].toLowerCase(); + } + else if (match = /^\s*#([0-9a-f])([0-9a-f])([0-9a-f])\s*$/i.exec(color)) { + return ('#' + match[1] + match[1] + match[2] + match[2] + match[3] + match[3]).toLowerCase(); + } + else if (match = /^\s*rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)\s*$/.exec(color)) { + var r = parseInt(match[1]), g = parseInt(match[2]), b = parseInt(match[3]); + r = (r < 16 ? '0' : '') + r.toString(16); + g = (g < 16 ? '0' : '') + g.toString(16); + b = (b < 16 ? '0' : '') + b.toString(16); + return '#' + r + g + b; + } + return false; + } + + function isColorDark(rgb) { + rgb = rgb.replace(/[\s#]/g, ''); + if (rgb.length == 3) { + rgb = rgb[0] + rgb[0] + rgb[1] + rgb[1] + rgb[2] + rgb[2]; + } + var r = parseInt(rgb.substr(0, 2), 16); + var g = parseInt(rgb.substr(2, 2), 16); + var b = parseInt(rgb.substr(4, 2), 16); + var hsp = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)); + return hsp < 120; + } + + function versionCompare(v1, v2) { + if (typeof v1 !== 'string') v1 = ''; + if (typeof v2 !== 'string') v2 = ''; + v1 = v1.replace(/^\s+|\s+$/g, '').split('.'); + v2 = v2.replace(/^\s+|\s+$/g, '').split('.'); + var a = Math.max(v1.length, v2.length), i, p1, p2; + for (i = 0; i < a; i++) { + p1 = parseInt(v1[i]) || 0; + p2 = parseInt(v2[i]) || 0; + if (p1 == p2) continue; + if (p1 > p2) return 1; + return -1; + } + return 0; + } + + function versionAtLeast(ver) { + return versionCompare(webAppVersion, ver) >= 0; + } + + function byteLength(str) { + if (window.Blob) { + try { return new Blob([str]).size; } catch (e) {} + } + var s = str.length; + for (var i=str.length-1; i>=0; i--) { + var code = str.charCodeAt(i); + if (code > 0x7f && code <= 0x7ff) s++; + else if (code > 0x7ff && code <= 0xffff) s+=2; + if (code >= 0xdc00 && code <= 0xdfff) i--; + } + return s; + } + + var BackButton = (function() { + var isVisible = false; + + var backButton = {}; + Object.defineProperty(backButton, 'isVisible', { + set: function(val){ setParams({is_visible: val}); }, + get: function(){ return isVisible; }, + enumerable: true + }); + + var curButtonState = null; + + WebView.onEvent('back_button_pressed', onBackButtonPressed); + + function onBackButtonPressed() { + receiveWebViewEvent('backButtonClicked'); + } + + function buttonParams() { + return {is_visible: isVisible}; + } + + function buttonState(btn_params) { + if (typeof btn_params === 'undefined') { + btn_params = buttonParams(); + } + return JSON.stringify(btn_params); + } + + function buttonCheckVersion() { + if (!versionAtLeast('6.1')) { + console.warn('[Telegram.WebApp] BackButton is not supported in version ' + webAppVersion); + return false; + } + return true; + } + + function updateButton() { + var btn_params = buttonParams(); + var btn_state = buttonState(btn_params); + if (curButtonState === btn_state) { + return; + } + curButtonState = btn_state; + WebView.postEvent('web_app_setup_back_button', false, btn_params); + } + + function setParams(params) { + if (!buttonCheckVersion()) { + return backButton; + } + if (typeof params.is_visible !== 'undefined') { + isVisible = !!params.is_visible; + } + updateButton(); + return backButton; + } + + backButton.onClick = function(callback) { + if (buttonCheckVersion()) { + onWebViewEvent('backButtonClicked', callback); + } + return backButton; + }; + backButton.offClick = function(callback) { + if (buttonCheckVersion()) { + offWebViewEvent('backButtonClicked', callback); + } + return backButton; + }; + backButton.show = function() { + return setParams({is_visible: true}); + }; + backButton.hide = function() { + return setParams({is_visible: false}); + }; + return backButton; + })(); + + var debugBottomBar = null, debugBottomBarBtns = {}, bottomBarHeight = 0; + if (initParams.tgWebAppDebug) { + debugBottomBar = document.createElement('tg-bottom-bar'); + var debugBottomBarStyle = { + display: 'flex', + gap: '7px', + font: '600 14px/18px sans-serif', + width: '100%', + background: getBottomBarColor(), + position: 'fixed', + left: '0', + right: '0', + bottom: '0', + margin: '0', + padding: '7px', + textAlign: 'center', + boxSizing: 'border-box', + zIndex: '10000' + }; + for (var k in debugBottomBarStyle) { + debugBottomBar.style[k] = debugBottomBarStyle[k]; + } + document.addEventListener('DOMContentLoaded', function onDomLoaded(event) { + document.removeEventListener('DOMContentLoaded', onDomLoaded); + document.body.appendChild(debugBottomBar); + }); + var animStyle = document.createElement('style'); + animStyle.innerHTML = 'tg-bottom-button.shine { position: relative; overflow: hidden; } tg-bottom-button.shine:before { content:""; position: absolute; top: 0; width: 100%; height: 100%; background: linear-gradient(120deg, transparent, rgba(255, 255, 255, .2), transparent); animation: tg-bottom-button-shine 5s ease-in-out infinite; } @-webkit-keyframes tg-bottom-button-shine { 0% {left: -100%;} 12%,100% {left: 100%}} @keyframes tg-bottom-button-shine { 0% {left: -100%;} 12%,100% {left: 100%}}'; + debugBottomBar.appendChild(animStyle); + } + function updateDebugBottomBar() { + var mainBtn = debugBottomBarBtns.main._bottomButton; + var secondaryBtn = debugBottomBarBtns.secondary._bottomButton; + if (mainBtn.isVisible || secondaryBtn.isVisible) { + debugBottomBar.style.display = 'flex'; + bottomBarHeight = 58; + if (mainBtn.isVisible && secondaryBtn.isVisible) { + if (secondaryBtn.position == 'top') { + debugBottomBar.style.flexDirection = 'column-reverse'; + bottomBarHeight += 51; + } else if (secondaryBtn.position == 'bottom') { + debugBottomBar.style.flexDirection = 'column'; + bottomBarHeight += 51; + } else if (secondaryBtn.position == 'left') { + debugBottomBar.style.flexDirection = 'row-reverse'; + } else if (secondaryBtn.position == 'right') { + debugBottomBar.style.flexDirection = 'row'; + } + } + } else { + debugBottomBar.style.display = 'none'; + bottomBarHeight = 0; + } + debugBottomBar.style.background = getBottomBarColor(); + if (document.documentElement) { + document.documentElement.style.boxSizing = 'border-box'; + document.documentElement.style.paddingBottom = bottomBarHeight + 'px'; + } + setViewportHeight(); + } + + + var BottomButtonConstructor = function(type) { + var isMainButton = (type == 'main'); + if (isMainButton) { + var setupFnName = 'web_app_setup_main_button'; + var tgEventName = 'main_button_pressed'; + var webViewEventName = 'mainButtonClicked'; + var buttonTextDefault = 'Continue'; + var buttonColorDefault = function(){ return themeParams.button_color || '#2481cc'; }; + var buttonTextColorDefault = function(){ return themeParams.button_text_color || '#ffffff'; }; + } else { + var setupFnName = 'web_app_setup_secondary_button'; + var tgEventName = 'secondary_button_pressed'; + var webViewEventName = 'secondaryButtonClicked'; + var buttonTextDefault = 'Cancel'; + var buttonColorDefault = function(){ return getBottomBarColor(); }; + var buttonTextColorDefault = function(){ return themeParams.button_color || '#2481cc'; }; + } + + var isVisible = false; + var isActive = true; + var hasShineEffect = false; + var isProgressVisible = false; + var iconCustomEmojiId = false; + var buttonType = type; + var buttonText = buttonTextDefault; + var buttonColor = false; + var buttonTextColor = false; + var buttonPosition = 'left'; + + var bottomButton = {}; + Object.defineProperty(bottomButton, 'type', { + get: function(){ return buttonType; }, + enumerable: true + }); + Object.defineProperty(bottomButton, 'iconCustomEmojiId', { + set: function(val){ bottomButton.setParams({icon_custom_emoji_id: val}); }, + get: function(){ return iconCustomEmojiId; }, + enumerable: true + }); + Object.defineProperty(bottomButton, 'text', { + set: function(val){ bottomButton.setParams({text: val}); }, + get: function(){ return buttonText; }, + enumerable: true + }); + Object.defineProperty(bottomButton, 'color', { + set: function(val){ bottomButton.setParams({color: val}); }, + get: function(){ return buttonColor || buttonColorDefault(); }, + enumerable: true + }); + Object.defineProperty(bottomButton, 'textColor', { + set: function(val){ bottomButton.setParams({text_color: val}); }, + get: function(){ return buttonTextColor || buttonTextColorDefault(); }, + enumerable: true + }); + Object.defineProperty(bottomButton, 'isVisible', { + set: function(val){ bottomButton.setParams({is_visible: val}); }, + get: function(){ return isVisible; }, + enumerable: true + }); + Object.defineProperty(bottomButton, 'isProgressVisible', { + get: function(){ return isProgressVisible; }, + enumerable: true + }); + Object.defineProperty(bottomButton, 'isActive', { + set: function(val){ bottomButton.setParams({is_active: val}); }, + get: function(){ return isActive; }, + enumerable: true + }); + Object.defineProperty(bottomButton, 'hasShineEffect', { + set: function(val){ bottomButton.setParams({has_shine_effect: val}); }, + get: function(){ return hasShineEffect; }, + enumerable: true + }); + if (!isMainButton) { + Object.defineProperty(bottomButton, 'position', { + set: function(val){ bottomButton.setParams({position: val}); }, + get: function(){ return buttonPosition; }, + enumerable: true + }); + } + + var curButtonState = null; + + WebView.onEvent(tgEventName, onBottomButtonPressed); + + var debugBtn = null; + if (initParams.tgWebAppDebug) { + debugBtn = document.createElement('tg-bottom-button'); + var debugBtnStyle = { + display: 'none', + width: '100%', + height: '44px', + borderRadius: '0', + background: 'no-repeat right center', + padding: '13px 15px', + textAlign: 'center', + boxSizing: 'border-box' + }; + for (var k in debugBtnStyle) { + debugBtn.style[k] = debugBtnStyle[k]; + } + debugBottomBar.appendChild(debugBtn); + debugBtn.addEventListener('click', onBottomButtonPressed, false); + debugBtn._bottomButton = bottomButton; + debugBottomBarBtns[type] = debugBtn; + } + + function onBottomButtonPressed() { + if (isActive) { + receiveWebViewEvent(webViewEventName); + } + } + + function buttonParams() { + var color = bottomButton.color; + var text_color = bottomButton.textColor; + if (isVisible) { + var params = { + is_visible: true, + is_active: isActive, + is_progress_visible: isProgressVisible, + icon_custom_emoji_id: iconCustomEmojiId, + text: buttonText, + color: color, + text_color: text_color, + has_shine_effect: hasShineEffect && isActive && !isProgressVisible + }; + if (!isMainButton) { + params.position = buttonPosition; + } + } else { + var params = { + is_visible: false + }; + } + return params; + } + + function buttonState(btn_params) { + if (typeof btn_params === 'undefined') { + btn_params = buttonParams(); + } + return JSON.stringify(btn_params); + } + + function updateButton() { + var btn_params = buttonParams(); + var btn_state = buttonState(btn_params); + if (curButtonState === btn_state) { + return; + } + curButtonState = btn_state; + WebView.postEvent(setupFnName, false, btn_params); + if (initParams.tgWebAppDebug) { + updateDebugButton(btn_params); + } + } + + function updateDebugButton(btn_params) { + if (btn_params.is_visible) { + debugBtn.style.display = 'block'; + + debugBtn.style.opacity = btn_params.is_active ? '1' : '0.8'; + debugBtn.style.cursor = btn_params.is_active ? 'pointer' : 'auto'; + debugBtn.disabled = !btn_params.is_active; + debugBtn.innerText = btn_params.text; + debugBtn.className = btn_params.has_shine_effect ? 'shine' : ''; + debugBtn.style.backgroundImage = btn_params.is_progress_visible ? "url('data:image/svg+xml," + encodeURIComponent('') + "')" : 'none'; + debugBtn.style.backgroundColor = btn_params.color; + debugBtn.style.color = btn_params.text_color; + } else { + debugBtn.style.display = 'none'; + } + updateDebugBottomBar(); + } + + function setParams(params) { + if (typeof params.icon_custom_emoji_id !== 'undefined') { + var emoji_id = params.icon_custom_emoji_id; + if (emoji_id === false || emoji_id === null) { + emoji_id = ''; + } + if (emoji_id !== '' && !/^[0-9]{10,20}$/.test(emoji_id)) { + console.error('[Telegram.WebApp] Bottom button icon custom emoji is invalid', params.icon_custom_emoji_id); + throw Error('WebAppBottomButtonParamInvalid'); + } + iconCustomEmojiId = emoji_id; + } + if (typeof params.text !== 'undefined') { + var text = strTrim(params.text); + if (!text.length && !iconCustomEmojiId) { + console.error('[Telegram.WebApp] Bottom button text is required', params.text); + throw Error('WebAppBottomButtonParamInvalid'); + } + if (text.length > 64) { + console.error('[Telegram.WebApp] Bottom button text is too long', text); + throw Error('WebAppBottomButtonParamInvalid'); + } + buttonText = text; + } + if (typeof params.color !== 'undefined') { + if (params.color === false || + params.color === null) { + buttonColor = false; + } else { + var color = parseColorToHex(params.color); + if (!color) { + console.error('[Telegram.WebApp] Bottom button color format is invalid', params.color); + throw Error('WebAppBottomButtonParamInvalid'); + } + buttonColor = color; + } + } + if (typeof params.text_color !== 'undefined') { + if (params.text_color === false || + params.text_color === null) { + buttonTextColor = false; + } else { + var text_color = parseColorToHex(params.text_color); + if (!text_color) { + console.error('[Telegram.WebApp] Bottom button text color format is invalid', params.text_color); + throw Error('WebAppBottomButtonParamInvalid'); + } + buttonTextColor = text_color; + } + } + if (typeof params.is_visible !== 'undefined') { + if (params.is_visible && + !bottomButton.text.length) { + console.error('[Telegram.WebApp] Bottom button text is required'); + throw Error('WebAppBottomButtonParamInvalid'); + } + isVisible = !!params.is_visible; + } + if (typeof params.has_shine_effect !== 'undefined') { + hasShineEffect = !!params.has_shine_effect; + } + if (!isMainButton && typeof params.position !== 'undefined') { + if (params.position != 'left' && params.position != 'right' && + params.position != 'top' && params.position != 'bottom') { + console.error('[Telegram.WebApp] Bottom button posiition is invalid', params.position); + throw Error('WebAppBottomButtonParamInvalid'); + } + buttonPosition = params.position; + } + if (typeof params.is_active !== 'undefined') { + isActive = !!params.is_active; + } + updateButton(); + return bottomButton; + } + + bottomButton.setText = function(text) { + return bottomButton.setParams({text: text}); + }; + bottomButton.onClick = function(callback) { + onWebViewEvent(webViewEventName, callback); + return bottomButton; + }; + bottomButton.offClick = function(callback) { + offWebViewEvent(webViewEventName, callback); + return bottomButton; + }; + bottomButton.show = function() { + return bottomButton.setParams({is_visible: true}); + }; + bottomButton.hide = function() { + return bottomButton.setParams({is_visible: false}); + }; + bottomButton.enable = function() { + return bottomButton.setParams({is_active: true}); + }; + bottomButton.disable = function() { + return bottomButton.setParams({is_active: false}); + }; + bottomButton.showProgress = function(leaveActive) { + isActive = !!leaveActive; + isProgressVisible = true; + updateButton(); + return bottomButton; + }; + bottomButton.hideProgress = function() { + if (!bottomButton.isActive) { + isActive = true; + } + isProgressVisible = false; + updateButton(); + return bottomButton; + } + bottomButton.setParams = setParams; + return bottomButton; + }; + var MainButton = BottomButtonConstructor('main'); + var SecondaryButton = BottomButtonConstructor('secondary'); + + var SettingsButton = (function() { + var isVisible = false; + + var settingsButton = {}; + Object.defineProperty(settingsButton, 'isVisible', { + set: function(val){ setParams({is_visible: val}); }, + get: function(){ return isVisible; }, + enumerable: true + }); + + var curButtonState = null; + + WebView.onEvent('settings_button_pressed', onSettingsButtonPressed); + + function onSettingsButtonPressed() { + receiveWebViewEvent('settingsButtonClicked'); + } + + function buttonParams() { + return {is_visible: isVisible}; + } + + function buttonState(btn_params) { + if (typeof btn_params === 'undefined') { + btn_params = buttonParams(); + } + return JSON.stringify(btn_params); + } + + function buttonCheckVersion() { + if (!versionAtLeast('6.10')) { + console.warn('[Telegram.WebApp] SettingsButton is not supported in version ' + webAppVersion); + return false; + } + return true; + } + + function updateButton() { + var btn_params = buttonParams(); + var btn_state = buttonState(btn_params); + if (curButtonState === btn_state) { + return; + } + curButtonState = btn_state; + WebView.postEvent('web_app_setup_settings_button', false, btn_params); + } + + function setParams(params) { + if (!buttonCheckVersion()) { + return settingsButton; + } + if (typeof params.is_visible !== 'undefined') { + isVisible = !!params.is_visible; + } + updateButton(); + return settingsButton; + } + + settingsButton.onClick = function(callback) { + if (buttonCheckVersion()) { + onWebViewEvent('settingsButtonClicked', callback); + } + return settingsButton; + }; + settingsButton.offClick = function(callback) { + if (buttonCheckVersion()) { + offWebViewEvent('settingsButtonClicked', callback); + } + return settingsButton; + }; + settingsButton.show = function() { + return setParams({is_visible: true}); + }; + settingsButton.hide = function() { + return setParams({is_visible: false}); + }; + return settingsButton; + })(); + + var HapticFeedback = (function() { + var hapticFeedback = {}; + + function triggerFeedback(params) { + if (!versionAtLeast('6.1')) { + console.warn('[Telegram.WebApp] HapticFeedback is not supported in version ' + webAppVersion); + return hapticFeedback; + } + if (params.type == 'impact') { + if (params.impact_style != 'light' && + params.impact_style != 'medium' && + params.impact_style != 'heavy' && + params.impact_style != 'rigid' && + params.impact_style != 'soft') { + console.error('[Telegram.WebApp] Haptic impact style is invalid', params.impact_style); + throw Error('WebAppHapticImpactStyleInvalid'); + } + } else if (params.type == 'notification') { + if (params.notification_type != 'error' && + params.notification_type != 'success' && + params.notification_type != 'warning') { + console.error('[Telegram.WebApp] Haptic notification type is invalid', params.notification_type); + throw Error('WebAppHapticNotificationTypeInvalid'); + } + } else if (params.type == 'selection_change') { + // no params needed + } else { + console.error('[Telegram.WebApp] Haptic feedback type is invalid', params.type); + throw Error('WebAppHapticFeedbackTypeInvalid'); + } + WebView.postEvent('web_app_trigger_haptic_feedback', false, params); + return hapticFeedback; + } + + hapticFeedback.impactOccurred = function(style) { + return triggerFeedback({type: 'impact', impact_style: style}); + }; + hapticFeedback.notificationOccurred = function(type) { + return triggerFeedback({type: 'notification', notification_type: type}); + }; + hapticFeedback.selectionChanged = function() { + return triggerFeedback({type: 'selection_change'}); + }; + return hapticFeedback; + })(); + + var CloudStorage = (function() { + var cloudStorage = {}; + + function invokeStorageMethod(method, params, callback) { + if (!versionAtLeast('6.9')) { + console.error('[Telegram.WebApp] CloudStorage is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + invokeCustomMethod(method, params, callback); + return cloudStorage; + } + + cloudStorage.setItem = function(key, value, callback) { + return invokeStorageMethod('saveStorageValue', {key: key, value: value}, callback); + }; + cloudStorage.getItem = function(key, callback) { + return cloudStorage.getItems([key], callback ? function(err, res) { + if (err) callback(err); + else callback(null, res[key]); + } : null); + }; + cloudStorage.getItems = function(keys, callback) { + return invokeStorageMethod('getStorageValues', {keys: keys}, callback); + }; + cloudStorage.removeItem = function(key, callback) { + return cloudStorage.removeItems([key], callback); + }; + cloudStorage.removeItems = function(keys, callback) { + return invokeStorageMethod('deleteStorageValues', {keys: keys}, callback); + }; + cloudStorage.getKeys = function(callback) { + return invokeStorageMethod('getStorageKeys', {}, callback); + }; + return cloudStorage; + })(); + + var DeviceStorage = (function() { + var deviceStorage = {}; + + WebView.onEvent('device_storage_key_saved', onDeviceStorageEvent); + WebView.onEvent('device_storage_key_received', onDeviceStorageEvent); + WebView.onEvent('device_storage_cleared', onDeviceStorageEvent); + WebView.onEvent('device_storage_failed', onDeviceStorageEvent); + + function onDeviceStorageEvent(eventType, eventData) { + if (eventData.req_id && webAppCallbacks[eventData.req_id]) { + var requestData = webAppCallbacks[eventData.req_id]; + delete webAppCallbacks[eventData.req_id]; + var res = null, err = null; + if (eventType == 'device_storage_failed') { + err = eventData.error || 'UNKNOWN_ERROR'; + } else if (eventType == 'device_storage_key_received') { + res = eventData.value; + } else { + res = true; + } + if (requestData.callback) { + requestData.callback(err, res); + } + } + } + + function invokeStorageMethod(method, params, callback) { + if (!versionAtLeast('9.0')) { + console.error('[Telegram.WebApp] DeviceStorage is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + var req_id = generateCallbackId(16); + var req_params = {req_id: req_id}; + for (var k in params) { + req_params[k] = params[k]; + } + webAppCallbacks[req_id] = { + callback: callback + }; + WebView.postEvent(method, false, req_params); + return deviceStorage; + } + + deviceStorage.setItem = function(key, value, callback) { + return invokeStorageMethod('web_app_device_storage_save_key', {key: key, value: value}, callback); + }; + deviceStorage.getItem = function(key, callback) { + return invokeStorageMethod('web_app_device_storage_get_key', {key: key}, callback); + }; + deviceStorage.removeItem = function(key, callback) { + return invokeStorageMethod('web_app_device_storage_save_key', {key: key, value: null}, callback); + }; + deviceStorage.clear = function(callback) { + return invokeStorageMethod('web_app_device_storage_clear', {}, callback); + }; + return deviceStorage; + })(); + + var SecureStorage = (function() { + var secureStorage = {}; + + WebView.onEvent('secure_storage_key_saved', onSecureStorageEvent); + WebView.onEvent('secure_storage_key_received', onSecureStorageEvent); + WebView.onEvent('secure_storage_key_restored', onSecureStorageEvent); + WebView.onEvent('secure_storage_cleared', onSecureStorageEvent); + WebView.onEvent('secure_storage_failed', onSecureStorageEvent); + + function onSecureStorageEvent(eventType, eventData) { + if (eventData.req_id && webAppCallbacks[eventData.req_id]) { + var requestData = webAppCallbacks[eventData.req_id]; + delete webAppCallbacks[eventData.req_id]; + var res = null, err = null, can_restore = null; + if (eventType == 'secure_storage_failed') { + err = eventData.error || 'UNKNOWN_ERROR'; + } else if (eventType == 'secure_storage_key_received') { + res = eventData.value; + if (eventData.can_restore) { + can_restore = true; + } + } else if (eventType == 'secure_storage_key_restored') { + res = eventData.value; + } else { + res = true; + } + if (requestData.callback) { + requestData.callback(err, res, can_restore); + } + } + } + + function invokeStorageMethod(method, params, callback) { + if (!versionAtLeast('9.0')) { + console.error('[Telegram.WebApp] SecureStorage is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + var req_id = generateCallbackId(16); + var req_params = {req_id: req_id}; + for (var k in params) { + req_params[k] = params[k]; + } + webAppCallbacks[req_id] = { + callback: callback + }; + WebView.postEvent(method, false, req_params); + return secureStorage; + } + + secureStorage.setItem = function(key, value, callback) { + return invokeStorageMethod('web_app_secure_storage_save_key', {key: key, value: value}, callback); + }; + secureStorage.getItem = function(key, callback) { + return invokeStorageMethod('web_app_secure_storage_get_key', {key: key}, callback); + }; + secureStorage.restoreItem = function(key, callback) { + return invokeStorageMethod('web_app_secure_storage_restore_key', {key: key}, callback); + }; + secureStorage.removeItem = function(key, callback) { + return invokeStorageMethod('web_app_secure_storage_save_key', {key: key, value: null}, callback); + }; + secureStorage.clear = function(callback) { + return invokeStorageMethod('web_app_secure_storage_clear', {}, callback); + }; + return secureStorage; + })(); + + var BiometricManager = (function() { + var isInited = false; + var isBiometricAvailable = false; + var biometricType = 'unknown'; + var isAccessRequested = false; + var isAccessGranted = false; + var isBiometricTokenSaved = false; + var deviceId = ''; + + var biometricManager = {}; + Object.defineProperty(biometricManager, 'isInited', { + get: function(){ return isInited; }, + enumerable: true + }); + Object.defineProperty(biometricManager, 'isBiometricAvailable', { + get: function(){ return isInited && isBiometricAvailable; }, + enumerable: true + }); + Object.defineProperty(biometricManager, 'biometricType', { + get: function(){ return biometricType || 'unknown'; }, + enumerable: true + }); + Object.defineProperty(biometricManager, 'isAccessRequested', { + get: function(){ return isAccessRequested; }, + enumerable: true + }); + Object.defineProperty(biometricManager, 'isAccessGranted', { + get: function(){ return isAccessRequested && isAccessGranted; }, + enumerable: true + }); + Object.defineProperty(biometricManager, 'isBiometricTokenSaved', { + get: function(){ return isBiometricTokenSaved; }, + enumerable: true + }); + Object.defineProperty(biometricManager, 'deviceId', { + get: function(){ return deviceId || ''; }, + enumerable: true + }); + + var initRequestState = {callbacks: []}; + var accessRequestState = false; + var authRequestState = false; + var tokenRequestState = false; + + WebView.onEvent('biometry_info_received', onBiometryInfoReceived); + WebView.onEvent('biometry_auth_requested', onBiometryAuthRequested); + WebView.onEvent('biometry_token_updated', onBiometryTokenUpdated); + + function onBiometryInfoReceived(eventType, eventData) { + isInited = true; + if (eventData.available) { + isBiometricAvailable = true; + biometricType = eventData.type || 'unknown'; + if (eventData.access_requested) { + isAccessRequested = true; + isAccessGranted = !!eventData.access_granted; + isBiometricTokenSaved = !!eventData.token_saved; + } else { + isAccessRequested = false; + isAccessGranted = false; + isBiometricTokenSaved = false; + } + } else { + isBiometricAvailable = false; + biometricType = 'unknown'; + isAccessRequested = false; + isAccessGranted = false; + isBiometricTokenSaved = false; + } + deviceId = eventData.device_id || ''; + + if (initRequestState.callbacks.length > 0) { + for (var i = 0; i < initRequestState.callbacks.length; i++) { + var callback = initRequestState.callbacks[i]; + callback(); + } + initRequestState.callbacks = []; + } + if (accessRequestState) { + var state = accessRequestState; + accessRequestState = false; + if (state.callback) { + state.callback(isAccessGranted); + } + } + receiveWebViewEvent('biometricManagerUpdated'); + } + function onBiometryAuthRequested(eventType, eventData) { + var isAuthenticated = (eventData.status == 'authorized'), + biometricToken = eventData.token || ''; + if (authRequestState) { + var state = authRequestState; + authRequestState = false; + if (state.callback) { + state.callback(isAuthenticated, isAuthenticated ? biometricToken : null); + } + } + receiveWebViewEvent('biometricAuthRequested', isAuthenticated ? { + isAuthenticated: true, + biometricToken: biometricToken + } : { + isAuthenticated: false + }); + } + function onBiometryTokenUpdated(eventType, eventData) { + var applied = false; + if (isBiometricAvailable && + isAccessRequested) { + if (eventData.status == 'updated') { + isBiometricTokenSaved = true; + applied = true; + } + else if (eventData.status == 'removed') { + isBiometricTokenSaved = false; + applied = true; + } + } + if (tokenRequestState) { + var state = tokenRequestState; + tokenRequestState = false; + if (state.callback) { + state.callback(applied); + } + } + receiveWebViewEvent('biometricTokenUpdated', { + isUpdated: applied + }); + } + + function checkVersion() { + if (!versionAtLeast('7.2')) { + console.warn('[Telegram.WebApp] BiometricManager is not supported in version ' + webAppVersion); + return false; + } + return true; + } + + function checkInit() { + if (!isInited) { + console.error('[Telegram.WebApp] BiometricManager should be inited before using.'); + throw Error('WebAppBiometricManagerNotInited'); + } + return true; + } + + biometricManager.init = function(callback) { + if (!checkVersion()) { + return biometricManager; + } + if (isInited) { + return biometricManager; + } + if (callback) { + initRequestState.callbacks.push(callback); + } + WebView.postEvent('web_app_biometry_get_info', false); + return biometricManager; + }; + biometricManager.requestAccess = function(params, callback) { + if (!checkVersion()) { + return biometricManager; + } + checkInit(); + if (!isBiometricAvailable) { + console.error('[Telegram.WebApp] Biometrics is not available on this device.'); + throw Error('WebAppBiometricManagerBiometricsNotAvailable'); + } + if (accessRequestState) { + console.error('[Telegram.WebApp] Access is already requested'); + throw Error('WebAppBiometricManagerAccessRequested'); + } + var popup_params = {}; + if (typeof params.reason !== 'undefined') { + var reason = strTrim(params.reason); + if (reason.length > 128) { + console.error('[Telegram.WebApp] Biometric reason is too long', reason); + throw Error('WebAppBiometricRequestAccessParamInvalid'); + } + if (reason.length > 0) { + popup_params.reason = reason; + } + } + + accessRequestState = { + callback: callback + }; + WebView.postEvent('web_app_biometry_request_access', false, popup_params); + return biometricManager; + }; + biometricManager.authenticate = function(params, callback) { + if (!checkVersion()) { + return biometricManager; + } + checkInit(); + if (!isBiometricAvailable) { + console.error('[Telegram.WebApp] Biometrics is not available on this device.'); + throw Error('WebAppBiometricManagerBiometricsNotAvailable'); + } + if (!isAccessGranted) { + console.error('[Telegram.WebApp] Biometric access was not granted by the user.'); + throw Error('WebAppBiometricManagerBiometricAccessNotGranted'); + } + if (authRequestState) { + console.error('[Telegram.WebApp] Authentication request is already in progress.'); + throw Error('WebAppBiometricManagerAuthenticationRequested'); + } + var popup_params = {}; + if (typeof params.reason !== 'undefined') { + var reason = strTrim(params.reason); + if (reason.length > 128) { + console.error('[Telegram.WebApp] Biometric reason is too long', reason); + throw Error('WebAppBiometricRequestAccessParamInvalid'); + } + if (reason.length > 0) { + popup_params.reason = reason; + } + } + + authRequestState = { + callback: callback + }; + WebView.postEvent('web_app_biometry_request_auth', false, popup_params); + return biometricManager; + }; + biometricManager.updateBiometricToken = function(token, callback) { + if (!checkVersion()) { + return biometricManager; + } + token = token || ''; + if (token.length > 1024) { + console.error('[Telegram.WebApp] Token is too long', token); + throw Error('WebAppBiometricManagerTokenInvalid'); + } + checkInit(); + if (!isBiometricAvailable) { + console.error('[Telegram.WebApp] Biometrics is not available on this device.'); + throw Error('WebAppBiometricManagerBiometricsNotAvailable'); + } + if (!isAccessGranted) { + console.error('[Telegram.WebApp] Biometric access was not granted by the user.'); + throw Error('WebAppBiometricManagerBiometricAccessNotGranted'); + } + if (tokenRequestState) { + console.error('[Telegram.WebApp] Token request is already in progress.'); + throw Error('WebAppBiometricManagerTokenUpdateRequested'); + } + tokenRequestState = { + callback: callback + }; + WebView.postEvent('web_app_biometry_update_token', false, {token: token}); + return biometricManager; + }; + biometricManager.openSettings = function() { + if (!checkVersion()) { + return biometricManager; + } + checkInit(); + if (!isBiometricAvailable) { + console.error('[Telegram.WebApp] Biometrics is not available on this device.'); + throw Error('WebAppBiometricManagerBiometricsNotAvailable'); + } + if (!isAccessRequested) { + console.error('[Telegram.WebApp] Biometric access was not requested yet.'); + throw Error('WebAppBiometricManagerBiometricsAccessNotRequested'); + } + if (isAccessGranted) { + console.warn('[Telegram.WebApp] Biometric access was granted by the user, no need to go to settings.'); + return biometricManager; + } + WebView.postEvent('web_app_biometry_open_settings', false); + return biometricManager; + }; + return biometricManager; + })(); + + var LocationManager = (function() { + var isInited = false; + var isLocationAvailable = false; + var isAccessRequested = false; + var isAccessGranted = false; + + var locationManager = {}; + Object.defineProperty(locationManager, 'isInited', { + get: function(){ return isInited; }, + enumerable: true + }); + Object.defineProperty(locationManager, 'isLocationAvailable', { + get: function(){ return isInited && isLocationAvailable; }, + enumerable: true + }); + Object.defineProperty(locationManager, 'isAccessRequested', { + get: function(){ return isAccessRequested; }, + enumerable: true + }); + Object.defineProperty(locationManager, 'isAccessGranted', { + get: function(){ return isAccessRequested && isAccessGranted; }, + enumerable: true + }); + + var initRequestState = {callbacks: []}; + var getRequestState = {callbacks: []}; + + WebView.onEvent('location_checked', onLocationChecked); + WebView.onEvent('location_requested', onLocationRequested); + + function onLocationChecked(eventType, eventData) { + isInited = true; + if (eventData.available) { + isLocationAvailable = true; + if (eventData.access_requested) { + isAccessRequested = true; + isAccessGranted = !!eventData.access_granted; + } else { + isAccessRequested = false; + isAccessGranted = false; + } + } else { + isLocationAvailable = false; + isAccessRequested = false; + isAccessGranted = false; + } + + if (initRequestState.callbacks.length > 0) { + for (var i = 0; i < initRequestState.callbacks.length; i++) { + var callback = initRequestState.callbacks[i]; + callback(); + } + initRequestState.callbacks = []; + } + receiveWebViewEvent('locationManagerUpdated'); + } + function onLocationRequested(eventType, eventData) { + if (!eventData.available) { + locationData = null; + } else { + var locationData = { + latitude: eventData.latitude, + longitude: eventData.longitude, + altitude: null, + course: null, + speed: null, + horizontal_accuracy: null, + vertical_accuracy: null, + course_accuracy: null, + speed_accuracy: null, + }; + if (typeof eventData.altitude !== 'undefined' && eventData.altitude !== null) { + locationData.altitude = eventData.altitude; + } + if (typeof eventData.course !== 'undefined' && eventData.course !== null) { + locationData.course = eventData.course % 360; + } + if (typeof eventData.speed !== 'undefined' && eventData.speed !== null) { + locationData.speed = eventData.speed; + } + if (typeof eventData.horizontal_accuracy !== 'undefined' && eventData.horizontal_accuracy !== null) { + locationData.horizontal_accuracy = eventData.horizontal_accuracy; + } + if (typeof eventData.vertical_accuracy !== 'undefined' && eventData.vertical_accuracy !== null) { + locationData.vertical_accuracy = eventData.vertical_accuracy; + } + if (typeof eventData.course_accuracy !== 'undefined' && eventData.course_accuracy !== null) { + locationData.course_accuracy = eventData.course_accuracy; + } + if (typeof eventData.speed_accuracy !== 'undefined' && eventData.speed_accuracy !== null) { + locationData.speed_accuracy = eventData.speed_accuracy; + } + } + if (!eventData.available || + !isLocationAvailable || + !isAccessRequested || + !isAccessGranted) { + initRequestState.callbacks.push(function() { + locationResponse(locationData); + }); + WebView.postEvent('web_app_check_location', false); + } else { + locationResponse(locationData); + } + } + function locationResponse(response) { + if (getRequestState.callbacks.length > 0) { + for (var i = 0; i < getRequestState.callbacks.length; i++) { + var callback = getRequestState.callbacks[i]; + callback(response); + } + getRequestState.callbacks = []; + } + if (response !== null) { + receiveWebViewEvent('locationRequested', { + locationData: response + }); + } + } + + function checkVersion() { + if (!versionAtLeast('8.0')) { + console.warn('[Telegram.WebApp] LocationManager is not supported in version ' + webAppVersion); + return false; + } + return true; + } + + function checkInit() { + if (!isInited) { + console.error('[Telegram.WebApp] LocationManager should be inited before using.'); + throw Error('WebAppLocationManagerNotInited'); + } + return true; + } + + locationManager.init = function(callback) { + if (!checkVersion()) { + return locationManager; + } + if (isInited) { + return locationManager; + } + if (callback) { + initRequestState.callbacks.push(callback); + } + WebView.postEvent('web_app_check_location', false); + return locationManager; + }; + locationManager.getLocation = function(callback) { + if (!checkVersion()) { + return locationManager; + } + checkInit(); + if (!isLocationAvailable) { + console.error('[Telegram.WebApp] Location is not available on this device.'); + throw Error('WebAppLocationManagerLocationNotAvailable'); + } + + getRequestState.callbacks.push(callback); + WebView.postEvent('web_app_request_location'); + return locationManager; + }; + locationManager.openSettings = function() { + if (!checkVersion()) { + return locationManager; + } + checkInit(); + if (!isLocationAvailable) { + console.error('[Telegram.WebApp] Location is not available on this device.'); + throw Error('WebAppLocationManagerLocationNotAvailable'); + } + if (!isAccessRequested) { + console.error('[Telegram.WebApp] Location access was not requested yet.'); + throw Error('WebAppLocationManagerLocationAccessNotRequested'); + } + if (isAccessGranted) { + console.warn('[Telegram.WebApp] Location access was granted by the user, no need to go to settings.'); + return locationManager; + } + WebView.postEvent('web_app_open_location_settings', false); + return locationManager; + }; + return locationManager; + })(); + + var Accelerometer = (function() { + var isStarted = false; + var valueX = null, valueY = null, valueZ = null; + var startCallbacks = [], stopCallbacks = []; + + var accelerometer = {}; + Object.defineProperty(accelerometer, 'isStarted', { + get: function(){ return isStarted; }, + enumerable: true + }); + Object.defineProperty(accelerometer, 'x', { + get: function(){ return valueX; }, + enumerable: true + }); + Object.defineProperty(accelerometer, 'y', { + get: function(){ return valueY; }, + enumerable: true + }); + Object.defineProperty(accelerometer, 'z', { + get: function(){ return valueZ; }, + enumerable: true + }); + + WebView.onEvent('accelerometer_started', onAccelerometerStarted); + WebView.onEvent('accelerometer_stopped', onAccelerometerStopped); + WebView.onEvent('accelerometer_changed', onAccelerometerChanged); + WebView.onEvent('accelerometer_failed', onAccelerometerFailed); + + function onAccelerometerStarted(eventType, eventData) { + isStarted = true; + if (startCallbacks.length > 0) { + for (var i = 0; i < startCallbacks.length; i++) { + var callback = startCallbacks[i]; + callback(true); + } + startCallbacks = []; + } + receiveWebViewEvent('accelerometerStarted'); + } + function onAccelerometerStopped(eventType, eventData) { + isStarted = false; + if (stopCallbacks.length > 0) { + for (var i = 0; i < stopCallbacks.length; i++) { + var callback = stopCallbacks[i]; + callback(true); + } + stopCallbacks = []; + } + receiveWebViewEvent('accelerometerStopped'); + } + function onAccelerometerChanged(eventType, eventData) { + valueX = eventData.x; + valueY = eventData.y; + valueZ = eventData.z; + receiveWebViewEvent('accelerometerChanged'); + } + function onAccelerometerFailed(eventType, eventData) { + if (startCallbacks.length > 0) { + for (var i = 0; i < startCallbacks.length; i++) { + var callback = startCallbacks[i]; + callback(false); + } + startCallbacks = []; + } + receiveWebViewEvent('accelerometerFailed', { + error: eventData.error + }); + } + + function checkVersion() { + if (!versionAtLeast('8.0')) { + console.warn('[Telegram.WebApp] Accelerometer is not supported in version ' + webAppVersion); + return false; + } + return true; + } + + accelerometer.start = function(params, callback) { + params = params || {}; + if (!checkVersion()) { + return accelerometer; + } + var req_params = {}; + var refresh_rate = parseInt(params.refresh_rate || 1000); + if (isNaN(refresh_rate) || refresh_rate < 20 || refresh_rate > 1000) { + console.warn('[Telegram.WebApp] Accelerometer refresh_rate is invalid', refresh_rate); + } else { + req_params.refresh_rate = refresh_rate; + } + + if (callback) { + startCallbacks.push(callback); + } + WebView.postEvent('web_app_start_accelerometer', false, req_params); + return accelerometer; + }; + accelerometer.stop = function(callback) { + if (!checkVersion()) { + return accelerometer; + } + if (callback) { + stopCallbacks.push(callback); + } + WebView.postEvent('web_app_stop_accelerometer'); + return accelerometer; + }; + return accelerometer; + })(); + + var DeviceOrientation = (function() { + var isStarted = false; + var valueAlpha = null, valueBeta = null, valueGamma = null, valueAbsolute = false; + var startCallbacks = [], stopCallbacks = []; + + var deviceOrientation = {}; + Object.defineProperty(deviceOrientation, 'isStarted', { + get: function(){ return isStarted; }, + enumerable: true + }); + Object.defineProperty(deviceOrientation, 'absolute', { + get: function(){ return valueAbsolute; }, + enumerable: true + }); + Object.defineProperty(deviceOrientation, 'alpha', { + get: function(){ return valueAlpha; }, + enumerable: true + }); + Object.defineProperty(deviceOrientation, 'beta', { + get: function(){ return valueBeta; }, + enumerable: true + }); + Object.defineProperty(deviceOrientation, 'gamma', { + get: function(){ return valueGamma; }, + enumerable: true + }); + + WebView.onEvent('device_orientation_started', onDeviceOrientationStarted); + WebView.onEvent('device_orientation_stopped', onDeviceOrientationStopped); + WebView.onEvent('device_orientation_changed', onDeviceOrientationChanged); + WebView.onEvent('device_orientation_failed', onDeviceOrientationFailed); + + function onDeviceOrientationStarted(eventType, eventData) { + isStarted = true; + if (startCallbacks.length > 0) { + for (var i = 0; i < startCallbacks.length; i++) { + var callback = startCallbacks[i]; + callback(true); + } + startCallbacks = []; + } + receiveWebViewEvent('deviceOrientationStarted'); + } + function onDeviceOrientationStopped(eventType, eventData) { + isStarted = false; + if (stopCallbacks.length > 0) { + for (var i = 0; i < stopCallbacks.length; i++) { + var callback = stopCallbacks[i]; + callback(true); + } + stopCallbacks = []; + } + receiveWebViewEvent('deviceOrientationStopped'); + } + function onDeviceOrientationChanged(eventType, eventData) { + valueAbsolute = !!eventData.absolute; + valueAlpha = eventData.alpha; + valueBeta = eventData.beta; + valueGamma = eventData.gamma; + receiveWebViewEvent('deviceOrientationChanged'); + } + function onDeviceOrientationFailed(eventType, eventData) { + if (startCallbacks.length > 0) { + for (var i = 0; i < startCallbacks.length; i++) { + var callback = startCallbacks[i]; + callback(false); + } + startCallbacks = []; + } + receiveWebViewEvent('deviceOrientationFailed', { + error: eventData.error + }); + } + + function checkVersion() { + if (!versionAtLeast('8.0')) { + console.warn('[Telegram.WebApp] DeviceOrientation is not supported in version ' + webAppVersion); + return false; + } + return true; + } + + deviceOrientation.start = function(params, callback) { + params = params || {}; + if (!checkVersion()) { + return deviceOrientation; + } + var req_params = {}; + var refresh_rate = parseInt(params.refresh_rate || 1000); + if (isNaN(refresh_rate) || refresh_rate < 20 || refresh_rate > 1000) { + console.warn('[Telegram.WebApp] DeviceOrientation refresh_rate is invalid', refresh_rate); + } else { + req_params.refresh_rate = refresh_rate; + } + req_params.need_absolute = !!params.need_absolute; + + if (callback) { + startCallbacks.push(callback); + } + WebView.postEvent('web_app_start_device_orientation', false, req_params); + return deviceOrientation; + }; + deviceOrientation.stop = function(callback) { + if (!checkVersion()) { + return deviceOrientation; + } + if (callback) { + stopCallbacks.push(callback); + } + WebView.postEvent('web_app_stop_device_orientation'); + return deviceOrientation; + }; + return deviceOrientation; + })(); + + var Gyroscope = (function() { + var isStarted = false; + var valueX = null, valueY = null, valueZ = null; + var startCallbacks = [], stopCallbacks = []; + + var gyroscope = {}; + Object.defineProperty(gyroscope, 'isStarted', { + get: function(){ return isStarted; }, + enumerable: true + }); + Object.defineProperty(gyroscope, 'x', { + get: function(){ return valueX; }, + enumerable: true + }); + Object.defineProperty(gyroscope, 'y', { + get: function(){ return valueY; }, + enumerable: true + }); + Object.defineProperty(gyroscope, 'z', { + get: function(){ return valueZ; }, + enumerable: true + }); + + WebView.onEvent('gyroscope_started', onGyroscopeStarted); + WebView.onEvent('gyroscope_stopped', onGyroscopeStopped); + WebView.onEvent('gyroscope_changed', onGyroscopeChanged); + WebView.onEvent('gyroscope_failed', onGyroscopeFailed); + + function onGyroscopeStarted(eventType, eventData) { + isStarted = true; + if (startCallbacks.length > 0) { + for (var i = 0; i < startCallbacks.length; i++) { + var callback = startCallbacks[i]; + callback(true); + } + startCallbacks = []; + } + receiveWebViewEvent('gyroscopeStarted'); + } + function onGyroscopeStopped(eventType, eventData) { + isStarted = false; + if (stopCallbacks.length > 0) { + for (var i = 0; i < stopCallbacks.length; i++) { + var callback = stopCallbacks[i]; + callback(true); + } + stopCallbacks = []; + } + receiveWebViewEvent('gyroscopeStopped'); + } + function onGyroscopeChanged(eventType, eventData) { + valueX = eventData.x; + valueY = eventData.y; + valueZ = eventData.z; + receiveWebViewEvent('gyroscopeChanged'); + } + function onGyroscopeFailed(eventType, eventData) { + if (startCallbacks.length > 0) { + for (var i = 0; i < startCallbacks.length; i++) { + var callback = startCallbacks[i]; + callback(false); + } + startCallbacks = []; + } + receiveWebViewEvent('gyroscopeFailed', { + error: eventData.error + }); + } + + function checkVersion() { + if (!versionAtLeast('8.0')) { + console.warn('[Telegram.WebApp] Gyroscope is not supported in version ' + webAppVersion); + return false; + } + return true; + } + + gyroscope.start = function(params, callback) { + params = params || {}; + if (!checkVersion()) { + return gyroscope; + } + var req_params = {}; + var refresh_rate = parseInt(params.refresh_rate || 1000); + if (isNaN(refresh_rate) || refresh_rate < 20 || refresh_rate > 1000) { + console.warn('[Telegram.WebApp] Gyroscope refresh_rate is invalid', refresh_rate); + } else { + req_params.refresh_rate = refresh_rate; + } + + if (callback) { + startCallbacks.push(callback); + } + WebView.postEvent('web_app_start_gyroscope', false, req_params); + return gyroscope; + }; + gyroscope.stop = function(callback) { + if (!checkVersion()) { + return gyroscope; + } + if (callback) { + stopCallbacks.push(callback); + } + WebView.postEvent('web_app_stop_gyroscope'); + return gyroscope; + }; + return gyroscope; + })(); + + var webAppInvoices = {}; + function onInvoiceClosed(eventType, eventData) { + if (eventData.slug && webAppInvoices[eventData.slug]) { + var invoiceData = webAppInvoices[eventData.slug]; + delete webAppInvoices[eventData.slug]; + if (invoiceData.callback) { + invoiceData.callback(eventData.status); + } + receiveWebViewEvent('invoiceClosed', { + url: invoiceData.url, + status: eventData.status + }); + } + } + + var webAppPopupOpened = false; + function onPopupClosed(eventType, eventData) { + if (webAppPopupOpened) { + var popupData = webAppPopupOpened; + webAppPopupOpened = false; + var button_id = null; + if (typeof eventData.button_id !== 'undefined') { + button_id = eventData.button_id; + } + if (popupData.callback) { + popupData.callback(button_id); + } + receiveWebViewEvent('popupClosed', { + button_id: button_id + }); + } + } + + var webAppScanQrPopupOpened = false; + function onQrTextReceived(eventType, eventData) { + if (webAppScanQrPopupOpened) { + var popupData = webAppScanQrPopupOpened; + var data = null; + if (typeof eventData.data !== 'undefined') { + data = eventData.data; + } + if (popupData.callback) { + if (popupData.callback(data)) { + webAppScanQrPopupOpened = false; + WebView.postEvent('web_app_close_scan_qr_popup', false); + } + } + receiveWebViewEvent('qrTextReceived', { + data: data + }); + } + } + function onScanQrPopupClosed(eventType, eventData) { + webAppScanQrPopupOpened = false; + receiveWebViewEvent('scanQrPopupClosed'); + } + + function onClipboardTextReceived(eventType, eventData) { + if (eventData.req_id && webAppCallbacks[eventData.req_id]) { + var requestData = webAppCallbacks[eventData.req_id]; + delete webAppCallbacks[eventData.req_id]; + var data = null; + if (typeof eventData.data !== 'undefined') { + data = eventData.data; + } + if (requestData.callback) { + requestData.callback(data); + } + receiveWebViewEvent('clipboardTextReceived', { + data: data + }); + } + } + + var WebAppWriteAccessRequested = false; + function onWriteAccessRequested(eventType, eventData) { + if (WebAppWriteAccessRequested) { + var requestData = WebAppWriteAccessRequested; + WebAppWriteAccessRequested = false; + if (requestData.callback) { + requestData.callback(eventData.status == 'allowed'); + } + receiveWebViewEvent('writeAccessRequested', { + status: eventData.status + }); + } + } + + function getRequestedContact(callback, timeout) { + var reqTo, fallbackTo, reqDelay = 0; + var reqInvoke = function() { + invokeCustomMethod('getRequestedContact', {}, function(err, res) { + if (res.substr(0, 1) == '"' && res.substr(-1) == '"') { // macos fix + res = JSON.parse(res); + } + if (res && res.length) { + clearTimeout(fallbackTo); + callback(res); + } else { + reqDelay += 50; + reqTo = setTimeout(reqInvoke, reqDelay); + } + }); + }; + var fallbackInvoke = function() { + clearTimeout(reqTo); + callback(''); + }; + fallbackTo = setTimeout(fallbackInvoke, timeout); + reqInvoke(); + } + + var WebAppContactRequested = false; + function onPhoneRequested(eventType, eventData) { + if (WebAppContactRequested) { + var requestData = WebAppContactRequested; + WebAppContactRequested = false; + var requestSent = eventData.status == 'sent'; + var webViewEvent = { + status: eventData.status + }; + if (requestSent) { + getRequestedContact(function(res) { + if (res && res.length) { + webViewEvent.response = res; + webViewEvent.responseUnsafe = Utils.urlParseQueryString(res); + for (var key in webViewEvent.responseUnsafe) { + var val = webViewEvent.responseUnsafe[key]; + try { + if (val.substr(0, 1) == '{' && val.substr(-1) == '}' || + val.substr(0, 1) == '[' && val.substr(-1) == ']') { + webViewEvent.responseUnsafe[key] = JSON.parse(val); + } + } catch (e) {} + } + } + if (requestData.callback) { + requestData.callback(requestSent, webViewEvent); + } + receiveWebViewEvent('contactRequested', webViewEvent); + }, 3000); + } else { + if (requestData.callback) { + requestData.callback(requestSent, webViewEvent); + } + receiveWebViewEvent('contactRequested', webViewEvent); + } + } + } + + var webAppDownloadFileRequested = false; + function onFileDownloadRequested(eventType, eventData) { + if (webAppDownloadFileRequested) { + var requestData = webAppDownloadFileRequested; + webAppDownloadFileRequested = false; + var isDownloading = eventData.status == 'downloading'; + if (requestData.callback) { + requestData.callback(isDownloading); + } + receiveWebViewEvent('fileDownloadRequested', { + status: isDownloading ? 'downloading' : 'cancelled' + }); + } + } + + function onCustomMethodInvoked(eventType, eventData) { + if (eventData.req_id && webAppCallbacks[eventData.req_id]) { + var requestData = webAppCallbacks[eventData.req_id]; + delete webAppCallbacks[eventData.req_id]; + var res = null, err = null; + if (typeof eventData.result !== 'undefined') { + res = eventData.result; + } + if (typeof eventData.error !== 'undefined') { + err = eventData.error; + } + if (requestData.callback) { + requestData.callback(err, res); + } + } + } + + function invokeCustomMethod(method, params, callback) { + if (!versionAtLeast('6.9')) { + console.error('[Telegram.WebApp] Method invokeCustomMethod is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + var req_id = generateCallbackId(16); + var req_params = {req_id: req_id, method: method, params: params || {}}; + webAppCallbacks[req_id] = { + callback: callback + }; + WebView.postEvent('web_app_invoke_custom_method', false, req_params); + }; + + if (!window.Telegram) { + window.Telegram = {}; + } + + Object.defineProperty(WebApp, 'initData', { + get: function(){ return webAppInitData; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'initDataUnsafe', { + get: function(){ return webAppInitDataUnsafe; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'version', { + get: function(){ return webAppVersion; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'platform', { + get: function(){ return webAppPlatform; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'colorScheme', { + get: function(){ return colorScheme; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'themeParams', { + get: function(){ return themeParams; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'isExpanded', { + get: function(){ return isExpanded; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'viewportHeight', { + get: function(){ return (viewportHeight === false ? window.innerHeight : viewportHeight) - bottomBarHeight; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'viewportStableHeight', { + get: function(){ return (viewportStableHeight === false ? window.innerHeight : viewportStableHeight) - bottomBarHeight; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'safeAreaInset', { + get: function(){ return safeAreaInset; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'contentSafeAreaInset', { + get: function(){ return contentSafeAreaInset; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'isClosingConfirmationEnabled', { + set: function(val){ setClosingConfirmation(val); }, + get: function(){ return isClosingConfirmationEnabled; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'isVerticalSwipesEnabled', { + set: function(val){ toggleVerticalSwipes(val); }, + get: function(){ return isVerticalSwipesEnabled; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'isFullscreen', { + get: function(){ return webAppIsFullscreen; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'isOrientationLocked', { + set: function(val){ toggleOrientationLock(val); }, + get: function(){ return webAppIsOrientationLocked; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'isActive', { + get: function(){ return webAppIsActive; }, + enumerable: true + }); + Object.defineProperty(WebApp, 'headerColor', { + set: function(val){ setHeaderColor(val); }, + get: function(){ return getHeaderColor(); }, + enumerable: true + }); + Object.defineProperty(WebApp, 'backgroundColor', { + set: function(val){ setBackgroundColor(val); }, + get: function(){ return getBackgroundColor(); }, + enumerable: true + }); + Object.defineProperty(WebApp, 'bottomBarColor', { + set: function(val){ setBottomBarColor(val); }, + get: function(){ return getBottomBarColor(); }, + enumerable: true + }); + Object.defineProperty(WebApp, 'BackButton', { + value: BackButton, + enumerable: true + }); + Object.defineProperty(WebApp, 'MainButton', { + value: MainButton, + enumerable: true + }); + Object.defineProperty(WebApp, 'SecondaryButton', { + value: SecondaryButton, + enumerable: true + }); + Object.defineProperty(WebApp, 'SettingsButton', { + value: SettingsButton, + enumerable: true + }); + Object.defineProperty(WebApp, 'HapticFeedback', { + value: HapticFeedback, + enumerable: true + }); + Object.defineProperty(WebApp, 'CloudStorage', { + value: CloudStorage, + enumerable: true + }); + Object.defineProperty(WebApp, 'DeviceStorage', { + value: DeviceStorage, + enumerable: true + }); + Object.defineProperty(WebApp, 'SecureStorage', { + value: SecureStorage, + enumerable: true + }); + Object.defineProperty(WebApp, 'BiometricManager', { + value: BiometricManager, + enumerable: true + }); + Object.defineProperty(WebApp, 'Accelerometer', { + value: Accelerometer, + enumerable: true + }); + Object.defineProperty(WebApp, 'DeviceOrientation', { + value: DeviceOrientation, + enumerable: true + }); + Object.defineProperty(WebApp, 'Gyroscope', { + value: Gyroscope, + enumerable: true + }); + Object.defineProperty(WebApp, 'LocationManager', { + value: LocationManager, + enumerable: true + }); + WebApp.isVersionAtLeast = function(ver) { + return versionAtLeast(ver); + }; + WebApp.setHeaderColor = function(color_key) { + WebApp.headerColor = color_key; + }; + WebApp.setBackgroundColor = function(color) { + WebApp.backgroundColor = color; + }; + WebApp.setBottomBarColor = function(color) { + WebApp.bottomBarColor = color; + }; + WebApp.enableClosingConfirmation = function() { + WebApp.isClosingConfirmationEnabled = true; + }; + WebApp.disableClosingConfirmation = function() { + WebApp.isClosingConfirmationEnabled = false; + }; + WebApp.enableVerticalSwipes = function() { + WebApp.isVerticalSwipesEnabled = true; + }; + WebApp.disableVerticalSwipes = function() { + WebApp.isVerticalSwipesEnabled = false; + }; + WebApp.lockOrientation = function() { + WebApp.isOrientationLocked = true; + }; + WebApp.unlockOrientation = function() { + WebApp.isOrientationLocked = false; + }; + WebApp.requestFullscreen = function() { + if (!versionAtLeast('8.0')) { + console.error('[Telegram.WebApp] Method requestFullscreen is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + WebView.postEvent('web_app_request_fullscreen'); + }; + WebApp.exitFullscreen = function() { + if (!versionAtLeast('8.0')) { + console.error('[Telegram.WebApp] Method exitFullscreen is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + WebView.postEvent('web_app_exit_fullscreen'); + }; + WebApp.addToHomeScreen = function() { + if (!versionAtLeast('8.0')) { + console.error('[Telegram.WebApp] Method addToHomeScreen is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + WebView.postEvent('web_app_add_to_home_screen'); + }; + WebApp.checkHomeScreenStatus = function(callback) { + if (!versionAtLeast('8.0')) { + console.error('[Telegram.WebApp] Method checkHomeScreenStatus is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + if (callback) { + homeScreenCallbacks.push(callback); + } + WebView.postEvent('web_app_check_home_screen'); + }; + WebApp.onEvent = function(eventType, callback) { + onWebViewEvent(eventType, callback); + }; + WebApp.offEvent = function(eventType, callback) {offWebViewEvent(eventType, callback); + }; + WebApp.sendData = function (data) { + if (!data || !data.length) { + console.error('[Telegram.WebApp] Data is required', data); + throw Error('WebAppDataInvalid'); + } + if (byteLength(data) > 4096) { + console.error('[Telegram.WebApp] Data is too long', data); + throw Error('WebAppDataInvalid'); + } + WebView.postEvent('web_app_data_send', false, {data: data}); + }; + WebApp.switchInlineQuery = function (query, choose_chat_types) { + if (!versionAtLeast('6.6')) { + console.error('[Telegram.WebApp] Method switchInlineQuery is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + if (!initParams.tgWebAppBotInline) { + console.error('[Telegram.WebApp] Inline mode is disabled for this bot. Read more about inline mode: https://core.telegram.org/bots/inline'); + throw Error('WebAppInlineModeDisabled'); + } + query = query || ''; + if (query.length > 256) { + console.error('[Telegram.WebApp] Inline query is too long', query); + throw Error('WebAppInlineQueryInvalid'); + } + var chat_types = []; + if (choose_chat_types) { + if (!Array.isArray(choose_chat_types)) { + console.error('[Telegram.WebApp] Choose chat types should be an array', choose_chat_types); + throw Error('WebAppInlineChooseChatTypesInvalid'); + } + var good_types = {users: 1, bots: 1, groups: 1, channels: 1}; + for (var i = 0; i < choose_chat_types.length; i++) { + var chat_type = choose_chat_types[i]; + if (!good_types[chat_type]) { + console.error('[Telegram.WebApp] Choose chat type is invalid', chat_type); + throw Error('WebAppInlineChooseChatTypeInvalid'); + } + if (good_types[chat_type] != 2) { + good_types[chat_type] = 2; + chat_types.push(chat_type); + } + } + } + WebView.postEvent('web_app_switch_inline_query', false, {query: query, chat_types: chat_types}); + }; + WebApp.openLink = function (url, options) { + var a = document.createElement('A'); + a.href = url; + if (a.protocol != 'http:' && + a.protocol != 'https:') { + console.error('[Telegram.WebApp] Url protocol is not supported', url); + throw Error('WebAppTgUrlInvalid'); + } + var url = a.href; + options = options || {}; + if (versionAtLeast('6.1')) { + var req_params = {url: url}; + if (versionAtLeast('6.4') && options.try_instant_view) { + req_params.try_instant_view = true; + } + if (versionAtLeast('7.6') && options.try_browser) { + req_params.try_browser = options.try_browser; + } + WebView.postEvent('web_app_open_link', false, req_params); + } else { + window.open(url, '_blank'); + } + }; + WebApp.openTelegramLink = function (url, options) { + var a = document.createElement('A'); + a.href = url; + if (a.protocol != 'http:' && + a.protocol != 'https:') { + console.error('[Telegram.WebApp] Url protocol is not supported', url); + throw Error('WebAppTgUrlInvalid'); + } + if (!isTmeHostname(a.hostname)) { + console.error('[Telegram.WebApp] Url host is not supported', url); + throw Error('WebAppTgUrlInvalid'); + } + var path_full = a.pathname + a.search; + options = options || {}; + if (isIframe || versionAtLeast('6.1')) { + var req_params = {path_full: path_full}; + if (options.force_request) { + req_params.force_request = true; + } + WebView.postEvent('web_app_open_tg_link', false, req_params); + } else { + location.href = 'https://t.me' + path_full; + } + }; + WebApp.openInvoice = function (url, callback) { + var a = document.createElement('A'), match, slug; + a.href = url; + if (a.protocol != 'http:' && + a.protocol != 'https:' || + !isTmeHostname(a.hostname) || + !(match = a.pathname.match(/^\/(\$|invoice\/)([A-Za-z0-9\-_=]+)$/)) || + !(slug = match[2])) { + console.error('[Telegram.WebApp] Invoice url is invalid', url); + throw Error('WebAppInvoiceUrlInvalid'); + } + if (!versionAtLeast('6.1')) { + console.error('[Telegram.WebApp] Method openInvoice is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + if (webAppInvoices[slug]) { + console.error('[Telegram.WebApp] Invoice is already opened'); + throw Error('WebAppInvoiceOpened'); + } + webAppInvoices[slug] = { + url: url, + callback: callback + }; + WebView.postEvent('web_app_open_invoice', false, {slug: slug}); + }; + WebApp.showPopup = function (params, callback) { + if (!versionAtLeast('6.2')) { + console.error('[Telegram.WebApp] Method showPopup is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + if (webAppPopupOpened) { + console.error('[Telegram.WebApp] Popup is already opened'); + throw Error('WebAppPopupOpened'); + } + var title = ''; + var message = ''; + var buttons = []; + var popup_buttons = {}; + var popup_params = {}; + if (typeof params.title !== 'undefined') { + title = strTrim(params.title); + if (title.length > 64) { + console.error('[Telegram.WebApp] Popup title is too long', title); + throw Error('WebAppPopupParamInvalid'); + } + if (title.length > 0) { + popup_params.title = title; + } + } + if (typeof params.message !== 'undefined') { + message = strTrim(params.message); + } + if (!message.length) { + console.error('[Telegram.WebApp] Popup message is required', params.message); + throw Error('WebAppPopupParamInvalid'); + } + if (message.length > 256) { + console.error('[Telegram.WebApp] Popup message is too long', message); + throw Error('WebAppPopupParamInvalid'); + } + popup_params.message = message; + if (typeof params.buttons !== 'undefined') { + if (!Array.isArray(params.buttons)) { + console.error('[Telegram.WebApp] Popup buttons should be an array', params.buttons); + throw Error('WebAppPopupParamInvalid'); + } + for (var i = 0; i < params.buttons.length; i++) { + var button = params.buttons[i]; + var btn = {}; + var id = ''; + if (typeof button.id !== 'undefined') { + id = button.id.toString(); + if (id.length > 64) { + console.error('[Telegram.WebApp] Popup button id is too long', id); + throw Error('WebAppPopupParamInvalid'); + } + } + btn.id = id; + var button_type = button.type; + if (typeof button_type === 'undefined') { + button_type = 'default'; + } + btn.type = button_type; + if (button_type == 'ok' || + button_type == 'close' || + button_type == 'cancel') { + // no params needed + } else if (button_type == 'default' || + button_type == 'destructive') { + var text = ''; + if (typeof button.text !== 'undefined') { + text = strTrim(button.text); + } + if (!text.length) { + console.error('[Telegram.WebApp] Popup button text is required for type ' + button_type, button.text); + throw Error('WebAppPopupParamInvalid'); + } + if (text.length > 64) { + console.error('[Telegram.WebApp] Popup button text is too long', text); + throw Error('WebAppPopupParamInvalid'); + } + btn.text = text; + } else { + console.error('[Telegram.WebApp] Popup button type is invalid', button_type); + throw Error('WebAppPopupParamInvalid'); + } + buttons.push(btn); + } + } else { + buttons.push({id: '', type: 'close'}); + } + if (buttons.length < 1) { + console.error('[Telegram.WebApp] Popup should have at least one button'); + throw Error('WebAppPopupParamInvalid'); + } + if (buttons.length > 3) { + console.error('[Telegram.WebApp] Popup should not have more than 3 buttons'); + throw Error('WebAppPopupParamInvalid'); + } + popup_params.buttons = buttons; + + webAppPopupOpened = { + callback: callback + }; + WebView.postEvent('web_app_open_popup', false, popup_params); + }; + WebApp.showAlert = function (message, callback) { + WebApp.showPopup({ + message: message + }, callback ? function(){ callback(); } : null); + }; + WebApp.showConfirm = function (message, callback) { + WebApp.showPopup({ + message: message, + buttons: [ + {type: 'ok', id: 'ok'}, + {type: 'cancel'} + ] + }, callback ? function (button_id) { + callback(button_id == 'ok'); + } : null); + }; + WebApp.showScanQrPopup = function (params, callback) { + if (!versionAtLeast('6.4')) { + console.error('[Telegram.WebApp] Method showScanQrPopup is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + if (webAppScanQrPopupOpened) { + console.error('[Telegram.WebApp] Popup is already opened'); + throw Error('WebAppScanQrPopupOpened'); + } + var text = ''; + var popup_params = {}; + if (typeof params.text !== 'undefined') { + text = strTrim(params.text); + if (text.length > 64) { + console.error('[Telegram.WebApp] Scan QR popup text is too long', text); + throw Error('WebAppScanQrPopupParamInvalid'); + } + if (text.length > 0) { + popup_params.text = text; + } + } + + webAppScanQrPopupOpened = { + callback: callback + }; + WebView.postEvent('web_app_open_scan_qr_popup', false, popup_params); + }; + WebApp.closeScanQrPopup = function () { + if (!versionAtLeast('6.4')) { + console.error('[Telegram.WebApp] Method closeScanQrPopup is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + + webAppScanQrPopupOpened = false; + WebView.postEvent('web_app_close_scan_qr_popup', false); + }; + WebApp.readTextFromClipboard = function (callback) { + if (!versionAtLeast('6.4')) { + console.error('[Telegram.WebApp] Method readTextFromClipboard is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + var req_id = generateCallbackId(16); + var req_params = {req_id: req_id}; + webAppCallbacks[req_id] = { + callback: callback + }; + WebView.postEvent('web_app_read_text_from_clipboard', false, req_params); + }; + WebApp.requestWriteAccess = function (callback) { + if (!versionAtLeast('6.9')) { + console.error('[Telegram.WebApp] Method requestWriteAccess is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + if (WebAppWriteAccessRequested) { + console.error('[Telegram.WebApp] Write access is already requested'); + throw Error('WebAppWriteAccessRequested'); + } + WebAppWriteAccessRequested = { + callback: callback + }; + WebView.postEvent('web_app_request_write_access'); + }; + WebApp.requestContact = function (callback) { + if (!versionAtLeast('6.9')) { + console.error('[Telegram.WebApp] Method requestContact is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + if (WebAppContactRequested) { + console.error('[Telegram.WebApp] Contact is already requested'); + throw Error('WebAppContactRequested'); + } + WebAppContactRequested = { + callback: callback + }; + WebView.postEvent('web_app_request_phone'); + }; + WebApp.downloadFile = function (params, callback) { + if (!versionAtLeast('8.0')) { + console.error('[Telegram.WebApp] Method downloadFile is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + if (webAppDownloadFileRequested) { + console.error('[Telegram.WebApp] Popup is already opened'); + throw Error('WebAppDownloadFilePopupOpened'); + } + var a = document.createElement('A'); + + var dl_params = {}; + if (!params || !params.url || !params.url.length) { + console.error('[Telegram.WebApp] Url is required'); + throw Error('WebAppDownloadFileParamInvalid'); + } + a.href = params.url; + if (a.protocol != 'https:') { + console.error('[Telegram.WebApp] Url protocol is not supported', url); + throw Error('WebAppDownloadFileParamInvalid'); + } + dl_params.url = a.href; + + if (!params || !params.file_name || !params.file_name.length) { + console.error('[Telegram.WebApp] File name is required'); + throw Error('WebAppDownloadFileParamInvalid'); + } + dl_params.file_name = params.file_name; + + webAppDownloadFileRequested = { + callback: callback + }; + WebView.postEvent('web_app_request_file_download', false, dl_params); + }; + WebApp.shareToStory = function (media_url, params) { + params = params || {}; + if (!versionAtLeast('7.8')) { + console.error('[Telegram.WebApp] Method shareToStory is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + var a = document.createElement('A'); + a.href = media_url; + if (a.protocol != 'http:' && + a.protocol != 'https:') { + console.error('[Telegram.WebApp] Media url protocol is not supported', url); + throw Error('WebAppMediaUrlInvalid'); + } + var share_params = {}; + share_params.media_url = a.href; + if (typeof params.text !== 'undefined') { + var text = strTrim(params.text); + if (text.length > 2048) { + console.error('[Telegram.WebApp] Text is too long', text); + throw Error('WebAppShareToStoryParamInvalid'); + } + if (text.length > 0) { + share_params.text = text; + } + } + if (typeof params.widget_link !== 'undefined') { + params.widget_link = params.widget_link || {}; + a.href = params.widget_link.url; + if (a.protocol != 'http:' && + a.protocol != 'https:') { + console.error('[Telegram.WebApp] Link protocol is not supported', url); + throw Error('WebAppShareToStoryParamInvalid'); + } + var widget_link = { + url: a.href + }; + if (typeof params.widget_link.name !== 'undefined') { + var link_name = strTrim(params.widget_link.name); + if (link_name.length > 48) { + console.error('[Telegram.WebApp] Link name is too long', link_name); + throw Error('WebAppShareToStoryParamInvalid'); + } + if (link_name.length > 0) { + widget_link.name = link_name; + } + } + share_params.widget_link = widget_link; + } + + WebView.postEvent('web_app_share_to_story', false, share_params); + }; + WebApp.shareMessage = function (msg_id, callback) { + if (!versionAtLeast('8.0')) { + console.error('[Telegram.WebApp] Method shareMessage is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + if (WebAppShareMessageOpened) { + console.error('[Telegram.WebApp] Share message is already opened'); + throw Error('WebAppShareMessageOpened'); + } + WebAppShareMessageOpened = { + callback: callback + }; + WebView.postEvent('web_app_send_prepared_message', false, {id: msg_id}); + }; + WebApp.requestChat = function (req_id, callback) { + if (!versionAtLeast('9.6')) { + console.error('[Telegram.WebApp] Method requestChat is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + if (WebAppRequestChatOpened) { + console.error('[Telegram.WebApp] Request chat is already opened'); + throw Error('WebAppRequestChatOpened'); + } + WebAppRequestChatOpened = { + callback: callback + }; + WebView.postEvent('web_app_request_chat', false, {req_id: req_id}); + }; + WebApp.setEmojiStatus = function (custom_emoji_id, params, callback) { + params = params || {}; + if (!versionAtLeast('8.0')) { + console.error('[Telegram.WebApp] Method setEmojiStatus is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + var status_params = {}; + status_params.custom_emoji_id = custom_emoji_id; + if (typeof params.duration !== 'undefined') { + status_params.duration = params.duration; + } + if (WebAppEmojiStatusRequested) { + console.error('[Telegram.WebApp] Emoji status is already requested'); + throw Error('WebAppEmojiStatusRequested'); + } + WebAppEmojiStatusRequested = { + callback: callback + }; + WebView.postEvent('web_app_set_emoji_status', false, status_params); + }; + WebApp.requestEmojiStatusAccess = function (callback) { + if (!versionAtLeast('8.0')) { + console.error('[Telegram.WebApp] Method requestEmojiStatusAccess is not supported in version ' + webAppVersion); + throw Error('WebAppMethodUnsupported'); + } + if (WebAppEmojiStatusAccessRequested) { + console.error('[Telegram.WebApp] Emoji status permission is already requested'); + throw Error('WebAppEmojiStatusAccessRequested'); + } + WebAppEmojiStatusAccessRequested = { + callback: callback + }; + WebView.postEvent('web_app_request_emoji_status_access'); + }; + WebApp.invokeCustomMethod = function (method, params, callback) { + invokeCustomMethod(method, params, callback); + }; + WebApp.hideKeyboard = function () { + WebView.postEvent('web_app_hide_keyboard'); + }; + WebApp.ready = function () { + WebView.postEvent('web_app_ready'); + }; + WebApp.expand = function () { + WebView.postEvent('web_app_expand'); + }; + WebApp.close = function (options) { + options = options || {}; + var req_params = {}; + if (versionAtLeast('7.6') && options.return_back) { + req_params.return_back = true; + } + WebView.postEvent('web_app_close', false, req_params); + }; + + window.Telegram.WebApp = WebApp; + + updateHeaderColor(); + updateBackgroundColor(); + updateBottomBarColor(); + setViewportHeight(); + if (initParams.tgWebAppShowSettings) { + SettingsButton.show(); + } + + window.addEventListener('resize', onWindowResize); + if (isIframe) { + document.addEventListener('click', linkHandler); + } + + WebView.onEvent('theme_changed', onThemeChanged); + WebView.onEvent('viewport_changed', onViewportChanged); + WebView.onEvent('safe_area_changed', onSafeAreaChanged); + WebView.onEvent('content_safe_area_changed', onContentSafeAreaChanged); + WebView.onEvent('visibility_changed', onVisibilityChanged); + WebView.onEvent('invoice_closed', onInvoiceClosed); + WebView.onEvent('popup_closed', onPopupClosed); + WebView.onEvent('qr_text_received', onQrTextReceived); + WebView.onEvent('scan_qr_popup_closed', onScanQrPopupClosed); + WebView.onEvent('clipboard_text_received', onClipboardTextReceived); + WebView.onEvent('write_access_requested', onWriteAccessRequested); + WebView.onEvent('phone_requested', onPhoneRequested); + WebView.onEvent('file_download_requested', onFileDownloadRequested); + WebView.onEvent('custom_method_invoked', onCustomMethodInvoked); + WebView.onEvent('fullscreen_changed', onFullscreenChanged); + WebView.onEvent('fullscreen_failed', onFullscreenFailed); + WebView.onEvent('home_screen_added', onHomeScreenAdded); + WebView.onEvent('home_screen_checked', onHomeScreenChecked); + WebView.onEvent('prepared_message_sent', onPreparedMessageSent); + WebView.onEvent('prepared_message_failed', onPreparedMessageFailed); + WebView.onEvent('requested_chat_sent', onRequestedChatSent); + WebView.onEvent('requested_chat_failed', onRequestedChatFailed); + WebView.onEvent('emoji_status_set', onEmojiStatusSet); + WebView.onEvent('emoji_status_failed', onEmojiStatusFailed); + WebView.onEvent('emoji_status_access_requested', onEmojiStatusAccessRequested); + WebView.postEvent('web_app_request_theme'); + WebView.postEvent('web_app_request_viewport'); + WebView.postEvent('web_app_request_safe_area'); + WebView.postEvent('web_app_request_content_safe_area'); + +})(); diff --git a/public/tg-app.js b/public/tg-app.js new file mode 100644 index 0000000..78dcc20 --- /dev/null +++ b/public/tg-app.js @@ -0,0 +1,32 @@ +// In-Telegram polish for every site page. No-ops in a normal browser; inside +// the Mini App webview it lazy-loads the vendored Telegram SDK (CSP stays +// script-src 'self') and wires theme + BackButton so pages feel native. +(function () { + 'use strict'; + var inTg = false; + try { inTg = sessionStorage.getItem('rmcTg') === '1'; } catch (e) {} + if (!inTg && (window.TelegramWebviewProxy !== undefined || /tgWebApp(Data|Platform)/.test(location.hash))) inTg = true; + if (!inTg) return; + try { sessionStorage.setItem('rmcTg', '1'); } catch (e) {} + + function boot() { + var tg = window.Telegram && window.Telegram.WebApp; + if (!tg || !tg.initData) return; + tg.ready(); + try { tg.expand(); } catch (e) {} + try { tg.setHeaderColor('#071421'); tg.setBackgroundColor('#071421'); } catch (e) {} + // Back button mirrors webview history: /app enters via location.replace, so + // the landing dashboard has no history and stays clean; any deeper page + // (tools, training, another member view) gets a native back arrow. + var bb = tg.BackButton; + if (bb && history.length > 1) { + bb.show(); + bb.onClick(function () { history.back(); }); + } + } + if (window.Telegram && window.Telegram.WebApp) { boot(); return; } + var s = document.createElement('script'); + s.src = '/telegram-web-app.js'; + s.onload = boot; + document.head.appendChild(s); +})(); diff --git a/public/tools.html b/public/tools.html index ce15bec..f218292 100644 --- a/public/tools.html +++ b/public/tools.html @@ -372,4 +372,4 @@ Watch the short training, look at the live payment feed, then ask me anything. - + diff --git a/public/training.html b/public/training.html index 3e04f51..1c8550b 100644 --- a/public/training.html +++ b/public/training.html @@ -83,4 +83,4 @@ - + diff --git a/public/weekly-rhythm.html b/public/weekly-rhythm.html index e5aad25..ed4e45a 100644 --- a/public/weekly-rhythm.html +++ b/public/weekly-rhythm.html @@ -82,4 +82,4 @@ - + diff --git a/server.js b/server.js index 015e6bf..fb474a8 100644 --- a/server.js +++ b/server.js @@ -50,7 +50,7 @@ FACTS: - Current team sponsor: ${a ? `ID ${a.id}${c.showSponsorName && a.name ? ` (${a.name})` : ''}, ${a.directs}/2 directs` : 'shown on the start page'}. ${waiting} placement(s) waiting. Placements rotate as positions qualify — always verify on https://rmcircle.team/start right before joining. - Site pages: https://rmcircle.team/ (strategy overview + roadmap + live team stats), https://rmcircle.team/start (current sponsor + join steps), https://rmcircle.team/training (THE CIRCLE METHOD — the team's free 10-lesson course in 3 modules. M1 Get Your Two: L1 mindset, L2 warm list, L3 the conversation, L4 objections. M2 Help Your Two: L5 dashboard-as-coaching-desk, L6 first 48 hours, L7 stalled people & pass-ups, L8 timing upgrades to catches. M3 Teach the Teachers: L9 run the same play, L10 the 20-minute weekly rhythm. ROUTING RULE — answer with the lesson: how do I find people→L2 (/training#lesson-2); what do I say→L3; pyramid objection→L4; new member just joined→L6; someone stalled→L7; should I upgrade→L8; overwhelmed→L10. Deep links: /training#lesson-N — plus 7 how-to videos — team overview, wallet setup, funding, the new connect-wallet join flow on the site, the dApp backup method, how payments work, and a full 14-min Member Dashboard walkthrough — + spillover article), https://rmcircle.team/how-pay-works (the two income streams shown as a pay-flow diagram + Premium/Standard tier comparison), https://rmcircle.team/contract (plain-language security review of the verified smart contract — code can't change, no pooled funds, locked rules, honest list of operator powers), https://rmcircle.team/weekly-rhythm (printable 20-minute Weekly Rhythm routine + 4-week habit tracker from Method Lesson 10, personalized like the Fast Start sheet), https://rmcircle.team/fast-start (printable 48-Hour Fast Start checklist — personalized with the member's invite link and a scannable QR code when opened from their dashboard; prints clean black-on-white, and prints in whatever language the member selected with the 🌐 button), https://rmcircle.team/my (member dashboard — its "Your team" panel opens with an organization bar: total members in your org, generations deep, qualified count below you, POL earned below you, and its approximate USD value at an hourly-cached POL price; the matrix under it drills leg by leg), https://rmcircle.team/tools (for existing team members who want to promote — share-ready promo videos (including the “Pocket Change” curiosity hook video — 25 ways people flush pocket change weekly with nothing to show for it, then the side-hustle flip; it deliberately shows no URL so the poster's invite link in the caption/description carries the credit, and matching pocket-change post copy sits in the Social posts section), copy-paste social posts, short/long email swipes, a downloadable banner kit in every standard size, and an Official RM Circle Media library (13 vertical social videos + 15 graphics from the creators — pair them with your own invite link in the caption; each curiosity video also has a MATCHED invite link (adds ?v= to the member's /join link) that makes the landing page continue that video's hook — recommend it when members ask which link to use with a video); open it from the gold Promo Tools button on your dashboard and every post/swipe arrives pre-personalized with YOUR invite link; to write promos in their own voice, mybrandedvoice.com), https://rmcircle.team/disclaimer (affiliate/earnings/risk disclosures). - UPGRADING FROM THE DASHBOARD: a qualified member can upgrade their level directly on their dashboard (rmcircle.team/my/) — an "Upgrade" card appears with the exact next-level cost read live from the contract; they connect the wallet that OWNS the position, confirm one transaction, done. The site never touches the funds (wallet pays the contract directly). If the wallet doesn't cover the cost, the card offers the MoonPay card-buy option. On phones, open the page inside the wallet app's browser. -- TELEGRAM COMPANION BOT: members can link their position (dashboard → Messages → "Connect Telegram", wallet-verified) to get instant payout DMs, native Telegram delivery of team messages (reply in Telegram to answer — matrix-line rules still apply), joined-on-your-link pings, and their invite/angle links via the "links" command. This finally lets members reach their downline as real people instead of just IDs — while handles stay private (the bot relays). +- TELEGRAM COMPANION BOT: members can link their position (dashboard → Messages → "Connect Telegram", wallet-verified) to get instant payout DMs, native Telegram delivery of team messages (reply in Telegram to answer — matrix-line rules still apply), joined-on-your-link pings, and their invite/angle links via the "links" command. This finally lets members reach their downline as real people instead of just IDs — while handles stay private (the bot relays). Linked members can also tap the bot's ☰ menu button to open the MINI APP — the full live dashboard, promo tools, and Circle Method training right inside Telegram with zero login (Telegram itself proves who they are). The website stays fully available too; the Mini App is a convenience door, not a replacement. - MESSAGES (on-site, wallet-verified): every member dashboard has a Messages panel — sign in once with the wallet that owns your position (a free signature, cannot move funds), then message your upline or anyone in your own team, or broadcast to your whole team. Spam-proof by design: messaging only works along your own matrix lines, so strangers can't message you. Unread messages show as a bell on your dashboard. Members are told the team admin can review messages for abuse. No email address needed. - BUYING POL WITH A CARD (for people brand new to crypto): the site links to MoonPay (moonpay.com/buy/pol) on the training page, the start page, and automatically on the join page when a connected wallet's balance is short. Guidance to give: choose POL on the POLYGON network, send it to YOUR OWN wallet address, buy about entry + gas (~385 POL). When explaining gas, use the car analogy: just like a car needs fuel to get anywhere, every blockchain transaction burns a tiny bit of POL to move — keep a little in the tank beyond the entry, because a wallet with an empty tank cannot make the trip. MoonPay is an independent company (merchant of record) — it handles ID verification and charges its own card fee (~4.5%); this site never touches or holds anyone's money. First purchases can take a few minutes to arrive. - LANGUAGE: always reply in the language the member writes in — translate program terms naturally and keep level names (Scintilla, Ascensus, ...) as-is. Site pages have a floating 🌐 Translate button (bottom-left) that machine-translates any page and remembers the choice. @@ -632,6 +632,23 @@ async function handleApi(req,res,pathname){ if(!r.url)return json(res,200,{error:'The Telegram bot is warming up — try again in a minute.'}); return json(res,200,{url:r.url,linked:!!tgbot.memberChat(s2.id)}); } + if(req.method==='POST'&&pathname==='/api/public/tg-webapp-auth'){ + // Telegram Mini App auth bridge: signed initData (HMAC-verified against the + // companion bot token) proves the Telegram account; the wallet-verified + // link in tg-links.json maps it to a member — so linked members land on + // their dashboard with zero login. Never creates links, only reads them. + const ip=String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim(); + if(memberLookupLimited(ip))return json(res,429,{error:'Too many requests — wait a minute.'}); + const b=await bodyJson(req).catch(()=>null); + if(!b||typeof b.initData!=='string')return json(res,400,{error:'Invalid request.'}); + const v=tgbot.verifyInitData(b.initData); + if(v.error)return json(res,401,{error:'Could not verify the Telegram launch data — close and reopen the app.'}); + const memberId=tgbot.chatMember(v.userId); + if(!memberId)return json(res,200,{ok:true,linked:false}); + const tok=messages.mintSession(memberId); + if(!tok)return json(res,500,{error:'Session error — try again.'}); + return json(res,200,{ok:true,linked:true,id:memberId},{'Set-Cookie':messages.sessionCookie(tok)}); + } if(req.method==='POST'&&pathname==='/api/public/msg-send'){ const s=messages.authFromCookie(req); if(!s)return json(res,401,{error:'Not signed in.'}); @@ -841,7 +858,7 @@ const server=http.createServer(async(req,res)=>{ if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]); } let file; - if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{ + if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{ const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file=''; } if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404); diff --git a/tgbot.js b/tgbot.js index e0280f0..eb3c61d 100644 --- a/tgbot.js +++ b/tgbot.js @@ -35,6 +35,28 @@ async function dm(chatId, text, extra) { return api('sendMessage', Object.assign({ chat_id: chatId, text, disable_web_page_preview: true }, extra || {})); } +// --- Mini App: verify Telegram WebApp initData (HMAC per Bot API spec) ------ +// secret_key = HMAC_SHA256(key="WebAppData", bot_token); hash covers the +// sorted key=value lines of every field except hash itself. +const INITDATA_MAX_AGE_S = 12 * 3600; +function verifyInitData(initData) { + const t = token(); if (!t) return { error: 'bot-offline' }; + if (typeof initData !== 'string' || !initData || initData.length > 4096) return { error: 'bad-initdata' }; + let params; try { params = new URLSearchParams(initData); } catch (e) { return { error: 'bad-initdata' }; } + const hash = params.get('hash'); + if (!hash || !/^[0-9a-f]{64}$/.test(hash)) return { error: 'bad-initdata' }; + params.delete('hash'); + const dcs = [...params.entries()].map(([k, v]) => `${k}=${v}`).sort().join('\n'); + const secret = crypto.createHmac('sha256', 'WebAppData').update(t).digest(); + const check = crypto.createHmac('sha256', secret).update(dcs).digest('hex'); + if (!crypto.timingSafeEqual(Buffer.from(check), Buffer.from(hash))) return { error: 'bad-hash' }; + const authDate = Number(params.get('auth_date')) || 0; + if (Math.abs(Date.now() / 1000 - authDate) > INITDATA_MAX_AGE_S) return { error: 'stale' }; + let user = null; try { user = JSON.parse(params.get('user') || 'null'); } catch (e) {} + if (!user || !user.id) return { error: 'no-user' }; + return { userId: user.id, user }; +} + // --- setup: learn our username + point the webhook at ourselves ------------- async function ensureWebhook(baseUrl) { const d = load(); @@ -42,6 +64,9 @@ async function ensureWebhook(baseUrl) { if (me && me.ok) { d.u = me.result.username; save(d); } const secret = webhookSecret(); if (!secret) return; + // Menu button (bottom-left ☰ in the private chat) opens the Mini App. + // Idempotent; BotFather /newapp is only needed for t.me// links. + await api('setChatMenuButton', { menu_button: { type: 'web_app', text: 'Open App', web_app: { url: `${baseUrl}/app` } } }); const url = `${baseUrl}/api/tg-hook/${secret}`; const info = await api('getWebhookInfo', {}); if (info && info.ok && info.result.url === url) return; @@ -228,7 +253,7 @@ async function handleUpdate(update) { d.members[String(rec.id)] = chatId; d.chats[String(chatId)] = rec.id; save(d); - await dm(chatId, `✅ Linked to position #${rec.id}!\n\nFrom now on:\n💰 You get a DM the moment your position catches a payment\n📨 Team messages reach you here — reply to answer\n🎉 You're pinged when someone joins on your link\n\nTry: coach · team · links — or: msg \nYour weekly 20-minute digest arrives Saturdays 10am Central (rhythm to adjust).`); + await dm(chatId, `✅ Linked to position #${rec.id}!\n\nFrom now on:\n💰 You get a DM the moment your position catches a payment\n📨 Team messages reach you here — reply to answer\n🎉 You're pinged when someone joins on your link\n\nTry: coach · team · links — or: msg \n📱 Tap the ☰ menu button (next to the message box) to open your full dashboard right inside Telegram — no login needed.\nYour weekly 20-minute digest arrives Saturdays 10am Central (rhythm to adjust).`); return; } await dm(chatId, `That link code is expired or already used. Get a fresh one from the Messages panel on your dashboard: https://rmcircle.team/my`); @@ -241,7 +266,7 @@ async function handleUpdate(update) { if (!linked) { await dm(chatId, `You're not linked yet. Open https://rmcircle.team/my → Messages → "Connect Telegram".`); return; } if (/^\/?links$/i.test(text)) { await dm(chatId, linksText(linked)); return; } - if (/^\/?help$/i.test(text)) { await dm(chatId, `Commands:\nlinks — your invite + angle links\ncoach — who to help + which lesson to send\nteam — your org numbers\nmsg — message a teammate\nlesson <1-10> — grab any Circle Method lesson link\nrhythm — your weekly 20-minute digest (rhythm now / rhythm sat 9 / rhythm off)\nReply to any 📨 message to answer it.\nDashboard: https://rmcircle.team/my/${linked}`); return; } + if (/^\/?help$/i.test(text)) { await dm(chatId, `Commands:\nlinks — your invite + angle links\ncoach — who to help + which lesson to send\nteam — your org numbers\nmsg — message a teammate\nlesson <1-10> — grab any Circle Method lesson link\nrhythm — your weekly 20-minute digest (rhythm now / rhythm sat 9 / rhythm off)\nReply to any 📨 message to answer it.\n📱 The ☰ menu button opens your full dashboard inside Telegram — no login.\nDashboard: https://rmcircle.team/my/${linked}`); return; } if (/^\/?coach$/i.test(text)) { const d = await fetchMember(linked); await dm(chatId, coachText(d)); return; } if (/^\/?team$/i.test(text)) { const d = await fetchMember(linked); await dm(chatId, teamText(d)); return; } @@ -275,4 +300,4 @@ async function handleUpdate(update) { } catch (e) { console.error('tgbot handleUpdate', e.message); } } -module.exports = { init, handleUpdate, notifyEvent, notifyMessage, makeLinkCode, webhookSecret, memberChat }; +module.exports = { init, handleUpdate, notifyEvent, notifyMessage, makeLinkCode, webhookSecret, memberChat, chatMember, verifyInitData };