telegram_webapp_sdk/webapp/
core.rs1use js_sys::{Function, Object, Promise, Reflect};
5use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
6use wasm_bindgen_futures::JsFuture;
7use web_sys::window;
8
9use crate::{core::context::TelegramContext, webapp::TelegramWebApp};
10
11pub(super) fn one_shot_promise<F>(f: F) -> Promise
16where
17 F: FnOnce(Function, Function) -> Result<(), JsValue>
18{
19 let mut executor = Some(f);
20 Promise::new(&mut |resolve, reject| {
21 let Some(invoke) = executor.take() else {
22 return;
23 };
24 if let Err(err) = invoke(resolve, reject.clone()) {
25 let _ = reject.call1(&JsValue::NULL, &err);
26 }
27 })
28}
29
30pub(super) async fn await_one_shot(promise: Promise) -> Result<JsValue, JsValue> {
31 JsFuture::from(promise).await
32}
33
34impl TelegramWebApp {
35 pub fn instance() -> Option<Self> {
37 let win = window()?;
38 let tg = Reflect::get(&win, &"Telegram".into()).ok()?;
39 let webapp = Reflect::get(&tg, &"WebApp".into()).ok()?;
40 webapp.dyn_into::<Object>().ok().map(|inner| Self {
41 inner
42 })
43 }
44
45 pub fn try_instance() -> Result<Self, JsValue> {
51 let win = window().ok_or_else(|| JsValue::from_str("window not available"))?;
52 let tg = Reflect::get(&win, &"Telegram".into())?;
53 let webapp = Reflect::get(&tg, &"WebApp".into())?;
54 let inner = webapp.dyn_into::<Object>()?;
55 Ok(Self {
56 inner
57 })
58 }
59
60 pub fn get_raw_init_data() -> Result<String, &'static str> {
84 TelegramContext::get_raw_init_data()
85 }
86
87 pub fn send_data(&self, data: &str) -> Result<(), JsValue> {
92 self.call1("sendData", &data.into())
93 }
94
95 pub fn is_version_at_least(&self, version: &str) -> Result<bool, JsValue> {
106 let f = Reflect::get(&self.inner, &"isVersionAtLeast".into())?;
107 let func = f
108 .dyn_ref::<Function>()
109 .ok_or_else(|| JsValue::from_str("isVersionAtLeast is not a function"))?;
110 let result = func.call1(&self.inner, &version.into())?;
111 Ok(result.as_bool().unwrap_or(false))
112 }
113
114 pub fn ready(&self) -> Result<(), JsValue> {
119 self.call0("ready")
120 }
121
122 pub fn invoke_custom_method_with_callback<F>(
133 &self,
134 method: &str,
135 params: &JsValue,
136 callback: F
137 ) -> Result<(), JsValue>
138 where
139 F: 'static + FnOnce(Result<JsValue, JsValue>)
140 {
141 let cb = Closure::once_into_js(move |err: JsValue, result: JsValue| {
142 if err.is_null() || err.is_undefined() {
143 callback(Ok(result));
144 } else {
145 callback(Err(err));
146 }
147 });
148 let f = Reflect::get(&self.inner, &"invokeCustomMethod".into())?;
149 let func = f
150 .dyn_ref::<Function>()
151 .ok_or_else(|| JsValue::from_str("invokeCustomMethod is not a function"))?;
152 func.call3(&self.inner, &method.into(), params, &cb)?;
153 Ok(())
154 }
155
156 pub async fn invoke_custom_method(
180 &self,
181 method: &str,
182 params: &JsValue
183 ) -> Result<JsValue, JsValue> {
184 let webapp = self.inner.clone();
185 let method = method.to_owned();
186 let params = params.clone();
187 let promise = one_shot_promise(move |resolve, reject| {
188 let resolve_for_cb = resolve.clone();
189 let reject_for_cb = reject.clone();
190 let cb = Closure::once_into_js(move |err: JsValue, result: JsValue| {
191 if err.is_null() || err.is_undefined() {
192 let _ = resolve_for_cb.call1(&JsValue::NULL, &result);
193 } else {
194 let _ = reject_for_cb.call1(&JsValue::NULL, &err);
195 }
196 });
197 let f = Reflect::get(&webapp, &"invokeCustomMethod".into())?;
198 let func = f
199 .dyn_ref::<Function>()
200 .ok_or_else(|| JsValue::from_str("invokeCustomMethod is not a function"))?;
201 func.call3(&webapp, &method.into(), ¶ms, &cb)?;
202 Ok(())
203 });
204 await_one_shot(promise).await
205 }
206
207 pub(super) fn call0(&self, method: &str) -> Result<(), JsValue> {
210 let f = Reflect::get(&self.inner, &method.into())?;
211 let func = f
212 .dyn_ref::<Function>()
213 .ok_or_else(|| JsValue::from_str("not a function"))?;
214 func.call0(&self.inner)?;
215 Ok(())
216 }
217
218 pub(super) fn call1(&self, method: &str, arg: &JsValue) -> Result<(), JsValue> {
219 let f = Reflect::get(&self.inner, &method.into())?;
220 let func = f
221 .dyn_ref::<Function>()
222 .ok_or_else(|| JsValue::from_str("not a function"))?;
223 func.call1(&self.inner, arg)?;
224 Ok(())
225 }
226
227 pub(super) fn call_nested0(&self, field: &str, method: &str) -> Result<(), JsValue> {
228 let obj = Reflect::get(&self.inner, &field.into())?;
229 let f = Reflect::get(&obj, &method.into())?;
230 let func = f
231 .dyn_ref::<Function>()
232 .ok_or_else(|| JsValue::from_str("not a function"))?;
233 func.call0(&obj)?;
234 Ok(())
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use std::{cell::RefCell, rc::Rc};
241
242 use js_sys::{Function, Object, Reflect};
243 use wasm_bindgen::JsValue;
244 use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
245 use web_sys::window;
246
247 use crate::webapp::TelegramWebApp;
248
249 wasm_bindgen_test_configure!(run_in_browser);
250
251 fn setup_webapp() -> Object {
252 let win = window().expect("window");
253 let telegram = Object::new();
254 let webapp = Object::new();
255 let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
256 let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
257 webapp
258 }
259
260 #[wasm_bindgen_test]
261 #[allow(dead_code, clippy::unused_unit)]
262 fn invoke_custom_method_with_callback_passes_args_and_delivers_result() {
263 let webapp = setup_webapp();
264 let invoke = Function::new_with_args(
265 "method, params, cb",
266 "this.method = method; this.params = params; cb(null, {ok: 1});"
267 );
268 let _ = Reflect::set(&webapp, &"invokeCustomMethod".into(), &invoke);
269
270 let app = TelegramWebApp::instance().expect("instance");
271 let received = Rc::new(RefCell::new(None::<JsValue>));
272 let cap = received.clone();
273 let params = Object::new();
274 let _ = Reflect::set(¶ms, &"x".into(), &"y".into());
275 app.invoke_custom_method_with_callback("doStuff", ¶ms.into(), move |out| {
276 *cap.borrow_mut() = Some(out.expect("ok"));
277 })
278 .expect("ok");
279
280 assert_eq!(
281 Reflect::get(&webapp, &"method".into())
282 .unwrap()
283 .as_string()
284 .as_deref(),
285 Some("doStuff")
286 );
287 let value = received.borrow().clone().expect("result");
288 let ok_val = Reflect::get(&value, &"ok".into()).expect("ok field");
289 assert_eq!(ok_val.as_f64(), Some(1.0));
290 }
291
292 #[wasm_bindgen_test]
293 #[allow(dead_code, clippy::unused_unit)]
294 fn invoke_custom_method_with_callback_translates_error() {
295 let webapp = setup_webapp();
296 let invoke = Function::new_with_args("_method, _params, cb", "cb('boom', null);");
297 let _ = Reflect::set(&webapp, &"invokeCustomMethod".into(), &invoke);
298
299 let app = TelegramWebApp::instance().expect("instance");
300 let received = Rc::new(RefCell::new(None::<JsValue>));
301 let cap = received.clone();
302 app.invoke_custom_method_with_callback("doStuff", &JsValue::NULL, move |out| {
303 *cap.borrow_mut() = Some(out.expect_err("err"));
304 })
305 .expect("ok");
306
307 let err = received.borrow().clone().expect("err");
308 assert_eq!(err.as_string().as_deref(), Some("boom"));
309 }
310
311 #[wasm_bindgen_test]
312 #[allow(dead_code, clippy::unused_unit)]
313 async fn invoke_custom_method_async_resolves_with_result() {
314 let webapp = setup_webapp();
315 let invoke = Function::new_with_args(
316 "_method, _params, cb",
317 "setTimeout(() => cb(null, {ok: 7}), 0);"
318 );
319 let _ = Reflect::set(&webapp, &"invokeCustomMethod".into(), &invoke);
320
321 let app = TelegramWebApp::instance().expect("instance");
322 let value = app
323 .invoke_custom_method("doStuff", &JsValue::NULL)
324 .await
325 .expect("resolved");
326 let ok = Reflect::get(&value, &"ok".into()).expect("ok field");
327 assert_eq!(ok.as_f64(), Some(7.0));
328 }
329
330 #[wasm_bindgen_test]
331 #[allow(dead_code, clippy::unused_unit)]
332 async fn invoke_custom_method_async_rejects_on_js_error() {
333 let webapp = setup_webapp();
334 let invoke = Function::new_with_args(
335 "_method, _params, cb",
336 "setTimeout(() => cb('boom', null), 0);"
337 );
338 let _ = Reflect::set(&webapp, &"invokeCustomMethod".into(), &invoke);
339
340 let app = TelegramWebApp::instance().expect("instance");
341 let err = app
342 .invoke_custom_method("doStuff", &JsValue::NULL)
343 .await
344 .expect_err("rejected");
345 assert_eq!(err.as_string().as_deref(), Some("boom"));
346 }
347}