telegram_webapp_sdk/webapp/core.rs
1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use js_sys::{Function, Object, Reflect};
5use wasm_bindgen::{JsCast, JsValue};
6use web_sys::window;
7
8use crate::{
9 core::context::TelegramContext,
10 validate_init_data::{self, ValidationKey},
11 webapp::TelegramWebApp
12};
13
14impl TelegramWebApp {
15 /// Get instance of `Telegram.WebApp` or `None` if not present
16 pub fn instance() -> Option<Self> {
17 let win = window()?;
18 let tg = Reflect::get(&win, &"Telegram".into()).ok()?;
19 let webapp = Reflect::get(&tg, &"WebApp".into()).ok()?;
20 webapp.dyn_into::<Object>().ok().map(|inner| Self {
21 inner
22 })
23 }
24
25 /// Try to get instance of `Telegram.WebApp`.
26 ///
27 /// # Errors
28 /// Returns [`JsValue`] if the `Telegram.WebApp` object is missing or
29 /// malformed.
30 pub fn try_instance() -> Result<Self, JsValue> {
31 let win = window().ok_or_else(|| JsValue::from_str("window not available"))?;
32 let tg = Reflect::get(&win, &"Telegram".into())?;
33 let webapp = Reflect::get(&tg, &"WebApp".into())?;
34 let inner = webapp.dyn_into::<Object>()?;
35 Ok(Self {
36 inner
37 })
38 }
39
40 /// Validate an `initData` payload using either HMAC-SHA256 or Ed25519.
41 ///
42 /// Pass [`ValidationKey::BotToken`] to verify the `hash` parameter using
43 /// the bot token. Use [`ValidationKey::Ed25519PublicKey`] to verify the
44 /// `signature` parameter with an Ed25519 public key.
45 ///
46 /// # Errors
47 /// Returns [`validate_init_data::ValidationError`] if validation fails.
48 ///
49 /// # Examples
50 /// ```no_run
51 /// use telegram_webapp_sdk::{TelegramWebApp, validate_init_data::ValidationKey};
52 /// let bot_token = "123456:ABC";
53 /// let query = "a=1&b=2&hash=9e5e8d7c0b1f9f3a";
54 /// TelegramWebApp::validate_init_data(query, ValidationKey::BotToken(bot_token)).unwrap();
55 /// ```
56 pub fn validate_init_data(
57 init_data: &str,
58 key: ValidationKey
59 ) -> Result<(), validate_init_data::ValidationError> {
60 match key {
61 ValidationKey::BotToken(token) => {
62 validate_init_data::verify_hmac_sha256(init_data, token)
63 }
64 ValidationKey::Ed25519PublicKey(pk) => {
65 validate_init_data::verify_ed25519(init_data, pk)
66 }
67 }
68 }
69
70 /// Returns the raw initData string as provided by Telegram.
71 ///
72 /// This is the URL-encoded initData string captured during SDK
73 /// initialization, suitable for server-side signature validation.
74 ///
75 /// # Errors
76 ///
77 /// Returns an error if the SDK has not been initialized via
78 /// [`crate::core::init::init_sdk`].
79 ///
80 /// # Examples
81 ///
82 /// ```no_run
83 /// use telegram_webapp_sdk::TelegramWebApp;
84 ///
85 /// match TelegramWebApp::get_raw_init_data() {
86 /// Ok(raw) => {
87 /// // Send to backend for validation
88 /// println!("Raw initData: {}", raw);
89 /// }
90 /// Err(e) => eprintln!("SDK not initialized: {}", e)
91 /// }
92 /// ```
93 pub fn get_raw_init_data() -> Result<String, &'static str> {
94 TelegramContext::get_raw_init_data()
95 }
96
97 /// Call `WebApp.sendData(data)`.
98 ///
99 /// # Errors
100 /// Returns [`JsValue`] if the underlying JS call fails.
101 pub fn send_data(&self, data: &str) -> Result<(), JsValue> {
102 self.call1("sendData", &data.into())
103 }
104
105 /// Returns whether the WebApp version is at least the provided value.
106 ///
107 /// # Examples
108 /// ```no_run
109 /// use telegram_webapp_sdk::webapp::TelegramWebApp;
110 ///
111 /// if let Some(app) = TelegramWebApp::instance() {
112 /// let _ = app.is_version_at_least("9.0");
113 /// }
114 /// ```
115 pub fn is_version_at_least(&self, version: &str) -> Result<bool, JsValue> {
116 let f = Reflect::get(&self.inner, &"isVersionAtLeast".into())?;
117 let func = f
118 .dyn_ref::<Function>()
119 .ok_or_else(|| JsValue::from_str("isVersionAtLeast is not a function"))?;
120 let result = func.call1(&self.inner, &version.into())?;
121 Ok(result.as_bool().unwrap_or(false))
122 }
123
124 /// Call `WebApp.ready()`.
125 ///
126 /// # Errors
127 /// Returns [`JsValue`] if the underlying JS call fails.
128 pub fn ready(&self) -> Result<(), JsValue> {
129 self.call0("ready")
130 }
131
132 // === Internal helper methods ===
133
134 pub(super) fn call0(&self, method: &str) -> Result<(), JsValue> {
135 let f = Reflect::get(&self.inner, &method.into())?;
136 let func = f
137 .dyn_ref::<Function>()
138 .ok_or_else(|| JsValue::from_str("not a function"))?;
139 func.call0(&self.inner)?;
140 Ok(())
141 }
142
143 pub(super) fn call1(&self, method: &str, arg: &JsValue) -> Result<(), JsValue> {
144 let f = Reflect::get(&self.inner, &method.into())?;
145 let func = f
146 .dyn_ref::<Function>()
147 .ok_or_else(|| JsValue::from_str("not a function"))?;
148 func.call1(&self.inner, arg)?;
149 Ok(())
150 }
151
152 pub(super) fn call_nested0(&self, field: &str, method: &str) -> Result<(), JsValue> {
153 let obj = Reflect::get(&self.inner, &field.into())?;
154 let f = Reflect::get(&obj, &method.into())?;
155 let func = f
156 .dyn_ref::<Function>()
157 .ok_or_else(|| JsValue::from_str("not a function"))?;
158 func.call0(&obj)?;
159 Ok(())
160 }
161}