diff --git a/declaration.d.ts b/declaration.d.ts index 2e2f0f4..0db1143 100644 --- a/declaration.d.ts +++ b/declaration.d.ts @@ -1,5 +1,3 @@ -import {InvoicePayload} from "./src/declarations/invoice-payload.interface"; - export interface WebAppUser { id: number; is_bot?: boolean; @@ -268,8 +266,38 @@ export interface Telegram { WebView: any; } +export interface InvoicePayload { + slug: string; + business_connection_id?: string; + title: string; + description: string; + payload: string; + provider_token?: string; + currency: string; + prices: { + label: string; + amount: number; + }[]; + subscription_period?: number; + max_tip_amount?: number; + suggested_tip_amounts?: number[]; + provider_data?: string; + photo_url?: string; + photo_size?: number; + photo_width?: number; + photo_height?: number; + need_name?: boolean; + need_phone_number?: boolean; + need_email?: boolean; + need_shipping_address?: boolean; + send_phone_number_to_provider?: boolean; + send_email_to_provider?: boolean; + is_flexible?: boolean; +} + declare global { interface Window { + __TELEGRAM_APPS_ANALYTICS: 1 | undefined, Telegram: Telegram, TelegramGameProxy: any, TelegramGameProxy_receiveEvent: (eventType: string, eventData: unknown) => void, diff --git a/package-lock.json b/package-lock.json index dba4578..7b3eb50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { "name": "@telegram-apps/analytics", - "version": "1.3.15-invoices", + "version": "1.4.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@telegram-apps/analytics", - "version": "1.3.15-invoices", + "version": "1.4.3", "license": "MIT", "dependencies": { - "@telegram-apps/sdk": "^1.1.0", "@tonconnect/ui": "2.0.5", "http-server": "14.1.1" }, @@ -740,10 +739,6 @@ "string-argv": "~0.3.1" } }, - "node_modules/@telegram-apps/sdk": { - "version": "1.1.3", - "license": "MIT" - }, "node_modules/@tonconnect/isomorphic-eventsource": { "version": "0.0.2", "license": "Apache-2.0", @@ -3807,9 +3802,6 @@ "string-argv": "~0.3.1" } }, - "@telegram-apps/sdk": { - "version": "1.1.3" - }, "@tonconnect/isomorphic-eventsource": { "version": "0.0.2", "requires": { diff --git a/package.json b/package.json index b69067b..aaa3502 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "wasm-pack": "^0.13.1" }, "dependencies": { - "@telegram-apps/sdk": "^1.1.0", "@tonconnect/ui": "2.0.5", "http-server": "14.1.1" }, diff --git a/src/app.ts b/src/app.ts index f3e53dd..d8631d7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,7 +3,7 @@ import { NetworkController } from './controllers/Network.controller' import { SessionController } from './controllers/Session.controller' import { BatchService } from "./services/Batch.service"; import { HumanProofService } from "./services/HumanProof.service"; -import {InvoicePayload} from "./declarations/invoice-payload.interface"; +import {InvoicePayload} from "../declaration"; import {Events} from "./constants"; export class App { @@ -16,8 +16,8 @@ export class App { private readonly apiToken: string; private readonly appName: string; - public taskParams: string; - public taskSolution: string | undefined; + public taskParams: string | undefined | null = null; + public env: 'STG' | 'PROD'; constructor(apiToken: string, appName: string, env: 'STG' | 'PROD') { @@ -35,9 +35,7 @@ export class App { public async init() { this.sessionController.init(); await this.analyticsController.init(); - await this.humanProofService.init().then(() => { - this.solveTask(); - }).catch(e => console.error(e)); + await this.humanProofService.init().catch(e => console.error(e)); this.networkController.init(); this.batchService.init(); } @@ -82,11 +80,11 @@ export class App { return this.appName; } - public solveTask() { - this.humanProofService.solveTask(); - } + public async solveHumanProofTask(): Promise { + if (this.taskParams === null) { + this.taskParams = await this.humanProofService.getInitialParams().catch(_err => undefined); + } - public setNewArgs(data: string) { - this.humanProofService.setNewArgs(data); + return await this.humanProofService.solveTask(this.taskParams).catch(_err => undefined); } } diff --git a/src/controllers/Analytics.controller.ts b/src/controllers/Analytics.controller.ts index 6db11ff..0ea6929 100644 --- a/src/controllers/Analytics.controller.ts +++ b/src/controllers/Analytics.controller.ts @@ -45,10 +45,6 @@ export class AnalyticsController { } } - public recordEvent(event_name: string, data?: Record) { - this.appModule.recordEvent(event_name, data).catch(e => console.error(e)); - } - public collectEvent(event_name: string, data?: Record) { if (this.eventsThreshold[event_name] === 0) { return; diff --git a/src/controllers/Network.controller.ts b/src/controllers/Network.controller.ts index ca7f505..3252fdc 100644 --- a/src/controllers/Network.controller.ts +++ b/src/controllers/Network.controller.ts @@ -32,13 +32,12 @@ export class NetworkController { return res; } - private readonly generateHeaders = (compressed: boolean) => { - this.appModule.solveTask(); - + private readonly generateHeaders = async (compressed: boolean) => { const conditionHeaders = {}; + const solution: string | undefined = await this.appModule.solveHumanProofTask(); - if (this.appModule.taskSolution) { - conditionHeaders["Content"] = this.appModule.taskSolution; + if (solution) { + conditionHeaders["Content"] = solution; } if (compressed) { @@ -58,36 +57,8 @@ export class NetworkController { ) { return await fetch(this.BACKEND_URL + 'events',{ method: 'POST', - headers: this.generateHeaders(compressed), + headers: await this.generateHeaders(compressed), body: compressed ? await compressData(data) : JSON.stringify(data), }).then(this.responseToParams, this.responseToParams); } - - public async recordEvent( - event_name: string, - data?: Record, - attributes?: Record, - compressed: boolean = true, - ) { - if (data?.custom_data) { - if (!attributes) { - attributes = data.custom_data; - } else { - attributes = Object.assign(data.custom_data, attributes); - } - } - - const body = { - ...data, - event_name: event_name, - custom_data: attributes, - ...this.appModule.assembleEventSession(), - }; - - await fetch(this.BACKEND_URL + 'events',{ - method: 'POST', - headers: this.generateHeaders(true), - body: compressed ? await compressData(body) : JSON.stringify(body), - }).then(this.responseToParams, this.responseToParams); - } } diff --git a/src/controllers/Session.controller.ts b/src/controllers/Session.controller.ts index 575fcd3..33ff412 100644 --- a/src/controllers/Session.controller.ts +++ b/src/controllers/Session.controller.ts @@ -1,16 +1,14 @@ -import { retrieveLaunchParams } from '@telegram-apps/sdk'; -import { WebAppUser } from '@twa-dev/types' import { App } from '../app' import { Errors, throwError } from '../errors' import { generateUUID } from '../utils/generateUUID'; +import { retrieveLaunchParams } from "../utils/retrieveLaunchParams"; +import { WebAppUser } from "../../declaration"; export class SessionController { private sessionId: string; - private userId: number; private userData: WebAppUser; private platform: string; private webAppStartParam: string; - private userLocale: string; private appModule: App; @@ -20,25 +18,15 @@ export class SessionController { public init() { const lp = retrieveLaunchParams(); - const initData = lp.initData; - const user = lp.initData?.user; - if (!user) { + const initData = lp.tgWebAppData; + + this.userData = initData?.user; + + if (!this.userData) { throwError(Errors.USER_DATA_IS_NOT_PROVIDED); } - this.userData = { - id: user.id, - is_premium: user.isPremium, - first_name: user.firstName, - is_bot: user.isBot, - last_name: user.lastName, - language_code: user.languageCode, - photo_url: user.photoUrl, - username: user.username, - }; - this.userId = user.id; - this.userLocale = user.languageCode; - this.webAppStartParam = initData.startParam; + this.webAppStartParam = initData.start_param; this.platform = lp.platform; this.sessionId = generateUUID(String(this.getUserId())); } @@ -48,7 +36,7 @@ export class SessionController { } public getUserId() { - return this.userId; + return this.userData.id; } public getWebAppStartParam() { @@ -60,7 +48,7 @@ export class SessionController { } public getUserLocale() { - return this.userLocale; + return this.userData.language_code; } public getUserData() { diff --git a/src/declarations/invoice-payload.interface.ts b/src/declarations/invoice-payload.interface.ts deleted file mode 100644 index 6df5d66..0000000 --- a/src/declarations/invoice-payload.interface.ts +++ /dev/null @@ -1,28 +0,0 @@ -export interface InvoicePayload { - slug: string; - business_connection_id?: string; - title: string; - description: string; - payload: string; - provider_token?: string; - currency: string; - prices: { - label: string; - amount: number; - }[]; - subscription_period?: number; - max_tip_amount?: number; - suggested_tip_amounts?: number[]; - provider_data?: string; - photo_url?: string; - photo_size?: number; - photo_width?: number; - photo_height?: number; - need_name?: boolean; - need_phone_number?: boolean; - need_email?: boolean; - need_shipping_address?: boolean; - send_phone_number_to_provider?: boolean; - send_email_to_provider?: boolean; - is_flexible?: boolean; -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 66cc3d9..f34c47a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { App } from './app' -import { InvoicePayload } from "./declarations/invoice-payload.interface"; -import {validateInvoicePayload} from "./validators/invoice-payload.validator"; +import { InvoicePayload } from "../declaration"; +import { validateInvoicePayload } from "./validators/invoice-payload.validator"; let __registerInvoice: (invoicePayload: InvoicePayload) => void; @@ -9,6 +9,12 @@ async function init({ token, appName, env = 'PROD'}: { appName: string, env?: 'STG' | 'PROD', }) { + if (window.__TELEGRAM_APPS_ANALYTICS) { + return; + } + + window.__TELEGRAM_APPS_ANALYTICS = 1; + const app = new App(token, appName, env); __registerInvoice = (invoicePayload: InvoicePayload) => { diff --git a/src/services/Batch.service.ts b/src/services/Batch.service.ts index 9ca5626..1a94ab7 100644 --- a/src/services/Batch.service.ts +++ b/src/services/Batch.service.ts @@ -19,40 +19,19 @@ export class BatchService { public init() { if (document.readyState === 'complete') { - this.startBatchingWithInterval(); + this.startBatching(); } else { document.onreadystatechange = () => { if (document.readyState == "complete") { - this.startBatchingWithInterval(); + this.startBatching(); } } } } - private startBatchingWithInterval() { - let counter = 0; - this.appModule.solveTask(); + private startBatching() { this.appModule.collectEvent(Events.INIT); - - if (this.appModule.taskSolution !== undefined) { - this.startBatching(); - } else { - const intervalId = setInterval(() => { - if (this.appModule.taskSolution !== undefined) { - this.startBatching(); - clearInterval(intervalId); - } else { - if (counter++ >= 3) { - this.startBatching() - clearInterval(intervalId); - - return; - } - - this.appModule.solveTask(); - } - }, 1000); - } + this.enableBatching(); } public stopBatching() { @@ -80,8 +59,7 @@ export class BatchService { } } - public startBatching() { - this.appModule.solveTask(); + public enableBatching() { if (this.intervalId === null) { this.intervalId = window.setInterval(() => this.processQueue(), this.batchInterval); } @@ -99,7 +77,7 @@ export class BatchService { this.stopBatching(); this.appModule.recordEvents(batch).then((res: Response)=> { if (String(res.status) === '429') { - this.startBatching(); + this.enableBatching(); return; } @@ -112,7 +90,7 @@ export class BatchService { if (this.backoff < 5){ this.backoff++; this.batchInterval = this.batchInterval * 2.71; - this.startBatching(); + this.enableBatching(); } return; } @@ -126,12 +104,12 @@ export class BatchService { this.appModule.taskSolution = undefined; if (this.taskRetry > 3) { - this.startBatching(); + this.enableBatching(); return; } else { this.appModule.solveTask(); - this.startBatching(); + this.enableBatching(); return; } @@ -143,10 +121,10 @@ export class BatchService { !batch.some(event => JSON.stringify(cachedEvent) === JSON.stringify(event))) ); - this.startBatching(); + this.enableBatching(); }, (error) => { console.log(error); - this.startBatching(); + this.enableBatching(); }); } } \ No newline at end of file diff --git a/src/services/HumanProof.service.ts b/src/services/HumanProof.service.ts index b5dc312..677380f 100644 --- a/src/services/HumanProof.service.ts +++ b/src/services/HumanProof.service.ts @@ -1,41 +1,56 @@ -import { App } from "../app"; -import { BACKEND_URL } from "../constants"; +import {App} from "../app"; +import {BACKEND_URL} from "../constants"; export class HumanProofService { - worker: Worker; + private worker: Worker; constructor(private readonly appModule: App) {} async init() { - return new Promise(async (resolve, reject) => { - try { - await fetch(BACKEND_URL + 'c3e068ebf11840ed3fc311a6f2df80b20fa05d25').then(async r => { - this.worker = new Worker(URL.createObjectURL(await r.blob())); - - await fetch(BACKEND_URL + 'aee7c93a9ae7930fb19732325d2c560c53849aa7').then(async res => { - this.appModule.taskParams = String(await res.text()); - - this.worker.onmessage = (event: MessageEvent) => { - this.appModule.taskSolution = event.data; - }; - - resolve(); - }); - }); - } catch (err) { - reject(err); - } + const workerScript = await fetch(BACKEND_URL + 'c3e068ebf11840ed3fc311a6f2df80b20fa05d25', { + signal: AbortSignal.timeout(3000) + }).catch(err => { + throw err; }); - } - public setNewArgs(data: string) { - this.appModule.taskParams = data - this.solveTask(); + if (workerScript instanceof Response) { + this.worker = new Worker(URL.createObjectURL(await workerScript.blob())); + + this.worker.onerror = err => { + console.error(err); + + this.worker = undefined; + } + } + + return true; } - solveTask() { - if (this.worker !== undefined) { - this.worker.postMessage(this.appModule.taskParams); + public async getInitialParams(): Promise { + if (this.worker === undefined) { + return undefined; } + + return await (await fetch(BACKEND_URL + 'c3e068ebf11840ed3fc311a6f2df80b20fa05d25', { + signal: AbortSignal.timeout(3000), + })).text(); + } + + public async solveTask(params: string): Promise { + const signal: AbortSignal = AbortSignal.timeout(1500); + + return new Promise((resolve, reject) => { + if (this.worker === undefined || signal.aborted || params === undefined) { + reject(); + } + + signal.addEventListener('abort', reject); + + this.worker.onmessage = (event: MessageEvent) => { + resolve(event.data) + }; + + this.worker.postMessage(params); + }); } } \ No newline at end of file diff --git a/src/utils/retrieveLaunchParams.ts b/src/utils/retrieveLaunchParams.ts new file mode 100644 index 0000000..23a6d73 --- /dev/null +++ b/src/utils/retrieveLaunchParams.ts @@ -0,0 +1,55 @@ +import { WebAppInitData } from "../../declaration"; +import { throwError } from "../errors"; + +export interface LaunchParams { + initDataRaw?: string; + tgWebAppData?: WebAppInitData; + version?: string; + platform?: string; + colorScheme?: 'light' | 'dark'; + [key: string]: any; +} + +function parseInitDataRaw(raw: string): WebAppInitData { + const parsed: Record = {}; + const searchParams = new URLSearchParams(raw); + + searchParams.forEach((value, key) => { + try { + parsed[key] = JSON.parse(value); + } catch { + parsed[key] = value; + } + }); + + return parsed as WebAppInitData; +} + +export function retrieveLaunchParams(): LaunchParams { + const hash = window.location.hash.substring(1); + const params: LaunchParams = {}; + + if (!hash) { + throwError('Can`t retrieve launch params'); + } + + hash.split('&').forEach(pair => { + const [key, value] = pair.split('='); + if (key && value) { + try { + const decoded = decodeURIComponent(value); + try { + params[key] = JSON.parse(decoded); + } catch { + params[key] = decoded; + } + } catch {} + } + }); + + if (typeof params.tgWebAppData === 'string') { + params.tgWebAppData = parseInitDataRaw(params.tgWebAppData); + } + + return params; +} \ No newline at end of file