use js_sys::{Function, Object, Promise, Reflect};
use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
use wasm_bindgen_futures::JsFuture;
use web_sys::window;
use crate::{core::context::TelegramContext, webapp::TelegramWebApp};
pub(super) fn one_shot_promise<F>(f: F) -> Promise
where
F: FnOnce(Function, Function) -> Result<(), JsValue>
{
let mut executor = Some(f);
Promise::new(&mut |resolve, reject| {
let Some(invoke) = executor.take() else {
return;
};
if let Err(err) = invoke(resolve, reject.clone()) {
let _ = reject.call1(&JsValue::NULL, &err);
}
})
}
pub(super) async fn await_one_shot(promise: Promise) -> Result<JsValue, JsValue> {
JsFuture::from(promise).await
}
impl TelegramWebApp {
pub fn instance() -> Option<Self> {
let win = window()?;
let tg = Reflect::get(&win, &"Telegram".into()).ok()?;
let webapp = Reflect::get(&tg, &"WebApp".into()).ok()?;
webapp.dyn_into::<Object>().ok().map(|inner| Self {
inner
})
}
pub fn try_instance() -> Result<Self, JsValue> {
let win = window().ok_or_else(|| JsValue::from_str("window not available"))?;
let tg = Reflect::get(&win, &"Telegram".into())?;
let webapp = Reflect::get(&tg, &"WebApp".into())?;
let inner = webapp.dyn_into::<Object>()?;
Ok(Self {
inner
})
}
pub fn get_raw_init_data() -> Result<String, &'static str> {
TelegramContext::get_raw_init_data()
}
pub fn send_data(&self, data: &str) -> Result<(), JsValue> {
self.call1("sendData", &data.into())
}
pub fn is_version_at_least(&self, version: &str) -> Result<bool, JsValue> {
let f = Reflect::get(&self.inner, &"isVersionAtLeast".into())?;
let func = f
.dyn_ref::<Function>()
.ok_or_else(|| JsValue::from_str("isVersionAtLeast is not a function"))?;
let result = func.call1(&self.inner, &version.into())?;
Ok(result.as_bool().unwrap_or(false))
}
pub fn ready(&self) -> Result<(), JsValue> {
self.call0("ready")
}
pub fn invoke_custom_method_with_callback<F>(
&self,
method: &str,
params: &JsValue,
callback: F
) -> Result<(), JsValue>
where
F: 'static + FnOnce(Result<JsValue, JsValue>)
{
let cb = Closure::once_into_js(move |err: JsValue, result: JsValue| {
if err.is_null() || err.is_undefined() {
callback(Ok(result));
} else {
callback(Err(err));
}
});
let f = Reflect::get(&self.inner, &"invokeCustomMethod".into())?;
let func = f
.dyn_ref::<Function>()
.ok_or_else(|| JsValue::from_str("invokeCustomMethod is not a function"))?;
func.call3(&self.inner, &method.into(), params, &cb)?;
Ok(())
}
pub async fn invoke_custom_method(
&self,
method: &str,
params: &JsValue
) -> Result<JsValue, JsValue> {
let webapp = self.inner.clone();
let method = method.to_owned();
let params = params.clone();
let promise = one_shot_promise(move |resolve, reject| {
let resolve_for_cb = resolve.clone();
let reject_for_cb = reject.clone();
let cb = Closure::once_into_js(move |err: JsValue, result: JsValue| {
if err.is_null() || err.is_undefined() {
let _ = resolve_for_cb.call1(&JsValue::NULL, &result);
} else {
let _ = reject_for_cb.call1(&JsValue::NULL, &err);
}
});
let f = Reflect::get(&webapp, &"invokeCustomMethod".into())?;
let func = f
.dyn_ref::<Function>()
.ok_or_else(|| JsValue::from_str("invokeCustomMethod is not a function"))?;
func.call3(&webapp, &method.into(), ¶ms, &cb)?;
Ok(())
});
await_one_shot(promise).await
}
pub(super) fn call0(&self, method: &str) -> Result<(), JsValue> {
let f = Reflect::get(&self.inner, &method.into())?;
let func = f
.dyn_ref::<Function>()
.ok_or_else(|| JsValue::from_str("not a function"))?;
func.call0(&self.inner)?;
Ok(())
}
pub(super) fn call1(&self, method: &str, arg: &JsValue) -> Result<(), JsValue> {
let f = Reflect::get(&self.inner, &method.into())?;
let func = f
.dyn_ref::<Function>()
.ok_or_else(|| JsValue::from_str("not a function"))?;
func.call1(&self.inner, arg)?;
Ok(())
}
pub(super) fn call_nested0(&self, field: &str, method: &str) -> Result<(), JsValue> {
let obj = Reflect::get(&self.inner, &field.into())?;
let f = Reflect::get(&obj, &method.into())?;
let func = f
.dyn_ref::<Function>()
.ok_or_else(|| JsValue::from_str("not a function"))?;
func.call0(&obj)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, rc::Rc};
use js_sys::{Function, Object, Reflect};
use wasm_bindgen::JsValue;
use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
use web_sys::window;
use crate::webapp::TelegramWebApp;
wasm_bindgen_test_configure!(run_in_browser);
fn setup_webapp() -> Object {
let win = window().expect("window");
let telegram = Object::new();
let webapp = Object::new();
let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
webapp
}
#[wasm_bindgen_test]
#[allow(dead_code, clippy::unused_unit)]
fn invoke_custom_method_with_callback_passes_args_and_delivers_result() {
let webapp = setup_webapp();
let invoke = Function::new_with_args(
"method, params, cb",
"this.method = method; this.params = params; cb(null, {ok: 1});"
);
let _ = Reflect::set(&webapp, &"invokeCustomMethod".into(), &invoke);
let app = TelegramWebApp::instance().expect("instance");
let received = Rc::new(RefCell::new(None::<JsValue>));
let cap = received.clone();
let params = Object::new();
let _ = Reflect::set(¶ms, &"x".into(), &"y".into());
app.invoke_custom_method_with_callback("doStuff", ¶ms.into(), move |out| {
*cap.borrow_mut() = Some(out.expect("ok"));
})
.expect("ok");
assert_eq!(
Reflect::get(&webapp, &"method".into())
.unwrap()
.as_string()
.as_deref(),
Some("doStuff")
);
let value = received.borrow().clone().expect("result");
let ok_val = Reflect::get(&value, &"ok".into()).expect("ok field");
assert_eq!(ok_val.as_f64(), Some(1.0));
}
#[wasm_bindgen_test]
#[allow(dead_code, clippy::unused_unit)]
fn invoke_custom_method_with_callback_translates_error() {
let webapp = setup_webapp();
let invoke = Function::new_with_args("_method, _params, cb", "cb('boom', null);");
let _ = Reflect::set(&webapp, &"invokeCustomMethod".into(), &invoke);
let app = TelegramWebApp::instance().expect("instance");
let received = Rc::new(RefCell::new(None::<JsValue>));
let cap = received.clone();
app.invoke_custom_method_with_callback("doStuff", &JsValue::NULL, move |out| {
*cap.borrow_mut() = Some(out.expect_err("err"));
})
.expect("ok");
let err = received.borrow().clone().expect("err");
assert_eq!(err.as_string().as_deref(), Some("boom"));
}
#[wasm_bindgen_test]
#[allow(dead_code, clippy::unused_unit)]
async fn invoke_custom_method_async_resolves_with_result() {
let webapp = setup_webapp();
let invoke = Function::new_with_args(
"_method, _params, cb",
"setTimeout(() => cb(null, {ok: 7}), 0);"
);
let _ = Reflect::set(&webapp, &"invokeCustomMethod".into(), &invoke);
let app = TelegramWebApp::instance().expect("instance");
let value = app
.invoke_custom_method("doStuff", &JsValue::NULL)
.await
.expect("resolved");
let ok = Reflect::get(&value, &"ok".into()).expect("ok field");
assert_eq!(ok.as_f64(), Some(7.0));
}
#[wasm_bindgen_test]
#[allow(dead_code, clippy::unused_unit)]
async fn invoke_custom_method_async_rejects_on_js_error() {
let webapp = setup_webapp();
let invoke = Function::new_with_args(
"_method, _params, cb",
"setTimeout(() => cb('boom', null), 0);"
);
let _ = Reflect::set(&webapp, &"invokeCustomMethod".into(), &invoke);
let app = TelegramWebApp::instance().expect("instance");
let err = app
.invoke_custom_method("doStuff", &JsValue::NULL)
.await
.expect_err("rejected");
assert_eq!(err.as_string().as_deref(), Some("boom"));
}
}