telegram_webapp_sdk/
webapp.rs

1use js_sys::{Function, Object, Reflect};
2use serde_wasm_bindgen::to_value;
3use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
4use web_sys::window;
5
6use crate::{
7    core::types::download_file_params::DownloadFileParams,
8    logger,
9    validate_init_data::{self, ValidationKey}
10};
11
12/// Handle returned when registering callbacks.
13pub struct EventHandle<T: ?Sized> {
14    target:   Object,
15    method:   &'static str,
16    event:    Option<String>,
17    callback: Closure<T>
18}
19
20impl<T: ?Sized> EventHandle<T> {
21    fn new(
22        target: Object,
23        method: &'static str,
24        event: Option<String>,
25        callback: Closure<T>
26    ) -> Self {
27        Self {
28            target,
29            method,
30            event,
31            callback
32        }
33    }
34
35    pub(crate) fn unregister(self) -> Result<(), JsValue> {
36        let f = Reflect::get(&self.target, &self.method.into())?;
37        let func = f
38            .dyn_ref::<Function>()
39            .ok_or_else(|| JsValue::from_str(&format!("{} is not a function", self.method)))?;
40        match self.event {
41            Some(event) => func.call2(
42                &self.target,
43                &event.into(),
44                self.callback.as_ref().unchecked_ref()
45            )?,
46            None => func.call1(&self.target, self.callback.as_ref().unchecked_ref())?
47        };
48        Ok(())
49    }
50}
51
52/// Identifies which bottom button to operate on.
53#[derive(Clone, Copy, Debug)]
54pub enum BottomButton {
55    /// Primary bottom button.
56    Main,
57    /// Secondary bottom button.
58    Secondary
59}
60
61impl BottomButton {
62    const fn js_name(self) -> &'static str {
63        match self {
64            BottomButton::Main => "MainButton",
65            BottomButton::Secondary => "SecondaryButton"
66        }
67    }
68}
69
70/// Background events delivered by Telegram when the Mini App runs in the
71/// background.
72#[derive(Clone, Copy, Debug)]
73pub enum BackgroundEvent {
74    /// The main button was clicked. Payload: [`JsValue::UNDEFINED`].
75    MainButtonClicked,
76    /// The back button was clicked. Payload: [`JsValue::UNDEFINED`].
77    BackButtonClicked,
78    /// The settings button was clicked. Payload: [`JsValue::UNDEFINED`].
79    SettingsButtonClicked,
80    /// User responded to a write access request. Payload: `bool`.
81    WriteAccessRequested,
82    /// User responded to a contact request. Payload: `bool`.
83    ContactRequested,
84    /// User responded to a phone number request. Payload: `bool`.
85    PhoneRequested,
86    /// An invoice was closed. Payload: status string.
87    InvoiceClosed,
88    /// A popup was closed. Payload: object containing `button_id`.
89    PopupClosed,
90    /// Text was received from the QR scanner. Payload: scanned text.
91    QrTextReceived,
92    /// Text was read from the clipboard. Payload: clipboard text.
93    ClipboardTextReceived
94}
95
96impl BackgroundEvent {
97    const fn as_str(self) -> &'static str {
98        match self {
99            BackgroundEvent::MainButtonClicked => "mainButtonClicked",
100            BackgroundEvent::BackButtonClicked => "backButtonClicked",
101            BackgroundEvent::SettingsButtonClicked => "settingsButtonClicked",
102            BackgroundEvent::WriteAccessRequested => "writeAccessRequested",
103            BackgroundEvent::ContactRequested => "contactRequested",
104            BackgroundEvent::PhoneRequested => "phoneRequested",
105            BackgroundEvent::InvoiceClosed => "invoiceClosed",
106            BackgroundEvent::PopupClosed => "popupClosed",
107            BackgroundEvent::QrTextReceived => "qrTextReceived",
108            BackgroundEvent::ClipboardTextReceived => "clipboardTextReceived"
109        }
110    }
111}
112
113/// Safe wrapper around `window.Telegram.WebApp`
114#[derive(Clone)]
115pub struct TelegramWebApp {
116    inner: Object
117}
118
119impl TelegramWebApp {
120    /// Get instance of `Telegram.WebApp` or `None` if not present
121    pub fn instance() -> Option<Self> {
122        let win = window()?;
123        let tg = Reflect::get(&win, &"Telegram".into()).ok()?;
124        let webapp = Reflect::get(&tg, &"WebApp".into()).ok()?;
125        webapp.dyn_into::<Object>().ok().map(|inner| Self {
126            inner
127        })
128    }
129
130    /// Try to get instance of `Telegram.WebApp`.
131    ///
132    /// # Errors
133    /// Returns [`JsValue`] if the `Telegram.WebApp` object is missing or
134    /// malformed.
135    pub fn try_instance() -> Result<Self, JsValue> {
136        let win = window().ok_or_else(|| JsValue::from_str("window not available"))?;
137        let tg = Reflect::get(&win, &"Telegram".into())?;
138        let webapp = Reflect::get(&tg, &"WebApp".into())?;
139        let inner = webapp.dyn_into::<Object>()?;
140        Ok(Self {
141            inner
142        })
143    }
144
145    /// Validate an `initData` payload using either HMAC-SHA256 or Ed25519.
146    ///
147    /// Pass [`ValidationKey::BotToken`] to verify the `hash` parameter using
148    /// the bot token. Use [`ValidationKey::Ed25519PublicKey`] to verify the
149    /// `signature` parameter with an Ed25519 public key.
150    ///
151    /// # Errors
152    /// Returns [`validate_init_data::ValidationError`] if validation fails.
153    ///
154    /// # Examples
155    /// ```no_run
156    /// use telegram_webapp_sdk::{TelegramWebApp, validate_init_data::ValidationKey};
157    /// let bot_token = "123456:ABC";
158    /// let query = "a=1&b=2&hash=9e5e8d7c0b1f9f3a";
159    /// TelegramWebApp::validate_init_data(query, ValidationKey::BotToken(bot_token)).unwrap();
160    /// ```
161    pub fn validate_init_data(
162        init_data: &str,
163        key: ValidationKey
164    ) -> Result<(), validate_init_data::ValidationError> {
165        match key {
166            ValidationKey::BotToken(token) => {
167                validate_init_data::verify_hmac_sha256(init_data, token)
168            }
169            ValidationKey::Ed25519PublicKey(pk) => {
170                validate_init_data::verify_ed25519(init_data, pk)
171            }
172        }
173    }
174
175    /// Call `WebApp.sendData(data)`.
176    ///
177    /// # Errors
178    /// Returns [`JsValue`] if the underlying JS call fails.
179    pub fn send_data(&self, data: &str) -> Result<(), JsValue> {
180        self.call1("sendData", &data.into())
181    }
182
183    /// Call `WebApp.expand()`.
184    ///
185    /// # Errors
186    /// Returns [`JsValue`] if the underlying JS call fails.
187    pub fn expand(&self) -> Result<(), JsValue> {
188        self.call0("expand")
189    }
190
191    /// Call `WebApp.close()`.
192    ///
193    /// # Errors
194    /// Returns [`JsValue`] if the underlying JS call fails.
195    pub fn close(&self) -> Result<(), JsValue> {
196        self.call0("close")
197    }
198
199    /// Call `WebApp.enableClosingConfirmation()`.
200    ///
201    /// # Examples
202    /// ```no_run
203    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
204    /// # let app = TelegramWebApp::instance().unwrap();
205    /// app.enable_closing_confirmation().unwrap();
206    /// ```
207    ///
208    /// # Errors
209    /// Returns [`JsValue`] if the underlying JS call fails.
210    pub fn enable_closing_confirmation(&self) -> Result<(), JsValue> {
211        self.call0("enableClosingConfirmation")
212    }
213
214    /// Call `WebApp.disableClosingConfirmation()`.
215    ///
216    /// # Examples
217    /// ```no_run
218    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
219    /// # let app = TelegramWebApp::instance().unwrap();
220    /// app.disable_closing_confirmation().unwrap();
221    /// ```
222    ///
223    /// # Errors
224    /// Returns [`JsValue`] if the underlying JS call fails.
225    pub fn disable_closing_confirmation(&self) -> Result<(), JsValue> {
226        self.call0("disableClosingConfirmation")
227    }
228
229    /// Returns whether closing confirmation is currently enabled.
230    ///
231    /// # Examples
232    /// ```no_run
233    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
234    /// # let app = TelegramWebApp::instance().unwrap();
235    /// let _ = app.is_closing_confirmation_enabled();
236    /// ```
237    pub fn is_closing_confirmation_enabled(&self) -> bool {
238        Reflect::get(&self.inner, &"isClosingConfirmationEnabled".into())
239            .ok()
240            .and_then(|v| v.as_bool())
241            .unwrap_or(false)
242    }
243
244    /// Call `WebApp.requestFullscreen()`.
245    ///
246    /// # Examples
247    /// ```no_run
248    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
249    /// # let app = TelegramWebApp::instance().unwrap();
250    /// app.request_fullscreen().unwrap();
251    /// ```
252    ///
253    /// # Errors
254    /// Returns [`JsValue`] if the underlying JS call fails.
255    pub fn request_fullscreen(&self) -> Result<(), JsValue> {
256        self.call0("requestFullscreen")
257    }
258
259    /// Call `WebApp.exitFullscreen()`.
260    ///
261    /// # Examples
262    /// ```no_run
263    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
264    /// # let app = TelegramWebApp::instance().unwrap();
265    /// app.exit_fullscreen().unwrap();
266    /// ```
267    ///
268    /// # Errors
269    /// Returns [`JsValue`] if the underlying JS call fails.
270    pub fn exit_fullscreen(&self) -> Result<(), JsValue> {
271        self.call0("exitFullscreen")
272    }
273
274    /// Call `WebApp.lockOrientation(orientation)`.
275    ///
276    /// # Examples
277    /// ```no_run
278    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
279    /// # let app = TelegramWebApp::instance().unwrap();
280    /// app.lock_orientation("portrait").unwrap();
281    /// ```
282    ///
283    /// # Errors
284    /// Returns [`JsValue`] if the underlying JS call fails.
285    pub fn lock_orientation(&self, orientation: &str) -> Result<(), JsValue> {
286        self.call1("lockOrientation", &orientation.into())
287    }
288
289    /// Call `WebApp.unlockOrientation()`.
290    ///
291    /// # Examples
292    /// ```no_run
293    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
294    /// # let app = TelegramWebApp::instance().unwrap();
295    /// app.unlock_orientation().unwrap();
296    /// ```
297    ///
298    /// # Errors
299    /// Returns [`JsValue`] if the underlying JS call fails.
300    pub fn unlock_orientation(&self) -> Result<(), JsValue> {
301        self.call0("unlockOrientation")
302    }
303
304    /// Call `WebApp.enableVerticalSwipes()`.
305    ///
306    /// # Examples
307    /// ```no_run
308    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
309    /// # let app = TelegramWebApp::instance().unwrap();
310    /// app.enable_vertical_swipes().unwrap();
311    /// ```
312    ///
313    /// # Errors
314    /// Returns [`JsValue`] if the underlying JS call fails.
315    pub fn enable_vertical_swipes(&self) -> Result<(), JsValue> {
316        self.call0("enableVerticalSwipes")
317    }
318
319    /// Call `WebApp.disableVerticalSwipes()`.
320    ///
321    /// # Examples
322    /// ```no_run
323    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
324    /// # let app = TelegramWebApp::instance().unwrap();
325    /// app.disable_vertical_swipes().unwrap();
326    /// ```
327    ///
328    /// # Errors
329    /// Returns [`JsValue`] if the underlying JS call fails.
330    pub fn disable_vertical_swipes(&self) -> Result<(), JsValue> {
331        self.call0("disableVerticalSwipes")
332    }
333
334    /// Call `WebApp.showAlert(message)`.
335    ///
336    /// # Errors
337    /// Returns [`JsValue`] if the underlying JS call fails.
338    pub fn show_alert(&self, msg: &str) -> Result<(), JsValue> {
339        self.call1("showAlert", &msg.into())
340    }
341
342    /// Call `WebApp.showConfirm(message, callback)`.
343    ///
344    /// # Errors
345    /// Returns [`JsValue`] if the underlying JS call fails.
346    pub fn show_confirm<F>(&self, msg: &str, on_confirm: F) -> Result<(), JsValue>
347    where
348        F: 'static + Fn(bool)
349    {
350        let cb = Closure::<dyn FnMut(bool)>::new(on_confirm);
351        let f = Reflect::get(&self.inner, &"showConfirm".into())?;
352        let func = f
353            .dyn_ref::<Function>()
354            .ok_or_else(|| JsValue::from_str("showConfirm is not a function"))?;
355        func.call2(&self.inner, &msg.into(), cb.as_ref().unchecked_ref())?;
356        cb.forget(); // safe leak for JS lifetime
357        Ok(())
358    }
359
360    /// Call `WebApp.openLink(url)`.
361    ///
362    /// # Examples
363    /// ```no_run
364    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
365    /// # let app = TelegramWebApp::instance().unwrap();
366    /// app.open_link("https://example.com").unwrap();
367    /// ```
368    pub fn open_link(&self, url: &str) -> Result<(), JsValue> {
369        Reflect::get(&self.inner, &"openLink".into())?
370            .dyn_into::<Function>()?
371            .call1(&self.inner, &url.into())?;
372        Ok(())
373    }
374
375    /// Call `WebApp.openTelegramLink(url)`.
376    ///
377    /// # Examples
378    /// ```no_run
379    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
380    /// # let app = TelegramWebApp::instance().unwrap();
381    /// app.open_telegram_link("https://t.me/telegram").unwrap();
382    /// ```
383    pub fn open_telegram_link(&self, url: &str) -> Result<(), JsValue> {
384        Reflect::get(&self.inner, &"openTelegramLink".into())?
385            .dyn_into::<Function>()?
386            .call1(&self.inner, &url.into())?;
387        Ok(())
388    }
389
390    /// Call `WebApp.openInvoice(url, callback)`.
391    ///
392    /// # Examples
393    /// ```no_run
394    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
395    /// # let app = TelegramWebApp::instance().unwrap();
396    /// app.open_invoice("https://invoice", |status| {
397    ///     let _ = status;
398    /// })
399    /// .unwrap();
400    /// ```
401    pub fn open_invoice<F>(&self, url: &str, callback: F) -> Result<(), JsValue>
402    where
403        F: 'static + Fn(String)
404    {
405        let cb = Closure::<dyn FnMut(JsValue)>::new(move |status: JsValue| {
406            callback(status.as_string().unwrap_or_default());
407        });
408        Reflect::get(&self.inner, &"openInvoice".into())?
409            .dyn_into::<Function>()?
410            .call2(&self.inner, &url.into(), cb.as_ref().unchecked_ref())?;
411        cb.forget();
412        Ok(())
413    }
414
415    /// Call `WebApp.switchInlineQuery(query, choose_chat_types)`.
416    ///
417    /// # Examples
418    /// ```no_run
419    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
420    /// # let app = TelegramWebApp::instance().unwrap();
421    /// app.switch_inline_query("query", None).unwrap();
422    /// ```
423    ///
424    /// # Errors
425    /// Returns [`JsValue`] if the underlying JS call fails.
426    pub fn switch_inline_query(
427        &self,
428        query: &str,
429        choose_chat_types: Option<&JsValue>
430    ) -> Result<(), JsValue> {
431        let f = Reflect::get(&self.inner, &"switchInlineQuery".into())?;
432        let func = f
433            .dyn_ref::<Function>()
434            .ok_or_else(|| JsValue::from_str("switchInlineQuery is not a function"))?;
435        match choose_chat_types {
436            Some(types) => func.call2(&self.inner, &query.into(), types)?,
437            None => func.call1(&self.inner, &query.into())?
438        };
439        Ok(())
440    }
441
442    /// Call `WebApp.shareMessage(msg_id, callback)`.
443    ///
444    /// # Examples
445    /// ```no_run
446    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
447    /// # let app = TelegramWebApp::instance().unwrap();
448    /// app.share_message("id123", |sent| {
449    ///     let _ = sent;
450    /// })
451    /// .unwrap();
452    /// ```
453    ///
454    /// # Errors
455    /// Returns [`JsValue`] if the underlying JS call fails.
456    pub fn share_message<F>(&self, msg_id: &str, callback: F) -> Result<(), JsValue>
457    where
458        F: 'static + Fn(bool)
459    {
460        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
461            callback(v.as_bool().unwrap_or(false));
462        });
463        let f = Reflect::get(&self.inner, &"shareMessage".into())?;
464        let func = f
465            .dyn_ref::<Function>()
466            .ok_or_else(|| JsValue::from_str("shareMessage is not a function"))?;
467        func.call2(&self.inner, &msg_id.into(), cb.as_ref().unchecked_ref())?;
468        cb.forget();
469        Ok(())
470    }
471
472    /// Call `WebApp.shareToStory(media_url, params)`.
473    ///
474    /// # Examples
475    /// ```no_run
476    /// # use js_sys::Object;
477    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
478    /// # let app = TelegramWebApp::instance().unwrap();
479    /// let params = Object::new();
480    /// app.share_to_story("https://example.com/image.png", Some(&params.into()))
481    ///     .unwrap();
482    /// ```
483    ///
484    /// # Errors
485    /// Returns [`JsValue`] if the underlying JS call fails.
486    pub fn share_to_story(
487        &self,
488        media_url: &str,
489        params: Option<&JsValue>
490    ) -> Result<(), JsValue> {
491        let f = Reflect::get(&self.inner, &"shareToStory".into())?;
492        let func = f
493            .dyn_ref::<Function>()
494            .ok_or_else(|| JsValue::from_str("shareToStory is not a function"))?;
495        match params {
496            Some(p) => func.call2(&self.inner, &media_url.into(), p)?,
497            None => func.call1(&self.inner, &media_url.into())?
498        };
499        Ok(())
500    }
501
502    /// Call `WebApp.shareURL(url, text)`.
503    ///
504    /// # Examples
505    /// ```no_run
506    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
507    /// # let app = TelegramWebApp::instance().unwrap();
508    /// app.share_url("https://example.com", Some("Check this"))
509    ///     .unwrap();
510    /// ```
511    ///
512    /// # Errors
513    /// Returns [`JsValue`] if the underlying JS call fails.
514    pub fn share_url(&self, url: &str, text: Option<&str>) -> Result<(), JsValue> {
515        let f = Reflect::get(&self.inner, &"shareURL".into())?;
516        let func = f
517            .dyn_ref::<Function>()
518            .ok_or_else(|| JsValue::from_str("shareURL is not a function"))?;
519        match text {
520            Some(t) => func.call2(&self.inner, &url.into(), &t.into())?,
521            None => func.call1(&self.inner, &url.into())?
522        };
523        Ok(())
524    }
525
526    /// Call `WebApp.joinVoiceChat(chat_id, invite_hash)`.
527    ///
528    /// # Examples
529    /// ```no_run
530    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
531    /// # let app = TelegramWebApp::instance().unwrap();
532    /// app.join_voice_chat("chat", None).unwrap();
533    /// ```
534    ///
535    /// # Errors
536    /// Returns [`JsValue`] if the underlying JS call fails.
537    pub fn join_voice_chat(
538        &self,
539        chat_id: &str,
540        invite_hash: Option<&str>
541    ) -> Result<(), JsValue> {
542        let f = Reflect::get(&self.inner, &"joinVoiceChat".into())?;
543        let func = f
544            .dyn_ref::<Function>()
545            .ok_or_else(|| JsValue::from_str("joinVoiceChat is not a function"))?;
546        match invite_hash {
547            Some(hash) => func.call2(&self.inner, &chat_id.into(), &hash.into())?,
548            None => func.call1(&self.inner, &chat_id.into())?
549        };
550        Ok(())
551    }
552
553    /// Call `WebApp.addToHomeScreen()` and return whether the prompt was shown.
554    ///
555    /// # Examples
556    /// ```no_run
557    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
558    /// # let app = TelegramWebApp::instance().unwrap();
559    /// let _shown = app.add_to_home_screen().unwrap();
560    /// ```
561    pub fn add_to_home_screen(&self) -> Result<bool, JsValue> {
562        let f = Reflect::get(&self.inner, &"addToHomeScreen".into())?;
563        let func = f
564            .dyn_ref::<Function>()
565            .ok_or_else(|| JsValue::from_str("addToHomeScreen is not a function"))?;
566        let result = func.call0(&self.inner)?;
567        Ok(result.as_bool().unwrap_or(false))
568    }
569
570    /// Call `WebApp.checkHomeScreenStatus(callback)`.
571    ///
572    /// # Examples
573    /// ```no_run
574    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
575    /// # let app = TelegramWebApp::instance().unwrap();
576    /// app.check_home_screen_status(|status| {
577    ///     let _ = status;
578    /// })
579    /// .unwrap();
580    /// ```
581    pub fn check_home_screen_status<F>(&self, callback: F) -> Result<(), JsValue>
582    where
583        F: 'static + Fn(String)
584    {
585        let cb = Closure::<dyn FnMut(JsValue)>::new(move |status: JsValue| {
586            callback(status.as_string().unwrap_or_default());
587        });
588        let f = Reflect::get(&self.inner, &"checkHomeScreenStatus".into())?;
589        let func = f
590            .dyn_ref::<Function>()
591            .ok_or_else(|| JsValue::from_str("checkHomeScreenStatus is not a function"))?;
592        func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
593        cb.forget();
594        Ok(())
595    }
596
597    /// Call `WebApp.requestWriteAccess(callback)`.
598    ///
599    /// # Examples
600    /// ```no_run
601    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
602    /// # let app = TelegramWebApp::instance().unwrap();
603    /// app.request_write_access(|granted| {
604    ///     let _ = granted;
605    /// })
606    /// .unwrap();
607    /// ```
608    ///
609    /// # Errors
610    /// Returns [`JsValue`] if the underlying JS call fails.
611    pub fn request_write_access<F>(&self, callback: F) -> Result<(), JsValue>
612    where
613        F: 'static + Fn(bool)
614    {
615        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
616            callback(v.as_bool().unwrap_or(false));
617        });
618        self.call1("requestWriteAccess", cb.as_ref().unchecked_ref())?;
619        cb.forget();
620        Ok(())
621    }
622
623    /// Call `WebApp.downloadFile(params, callback)`.
624    ///
625    /// # Examples
626    /// ```no_run
627    /// # use telegram_webapp_sdk::core::types::download_file_params::DownloadFileParams;
628    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
629    /// # let app = TelegramWebApp::instance().unwrap();
630    /// let params = DownloadFileParams {
631    ///     url:       "https://example.com/file",
632    ///     file_name: None,
633    ///     mime_type: None
634    /// };
635    /// app.download_file(params, |file_id| {
636    ///     let _ = file_id;
637    /// })
638    /// .unwrap();
639    /// ```
640    ///
641    /// # Errors
642    /// Returns [`JsValue`] if the underlying JS call fails or the parameters
643    /// fail to serialize.
644    pub fn download_file<F>(
645        &self,
646        params: DownloadFileParams<'_>,
647        callback: F
648    ) -> Result<(), JsValue>
649    where
650        F: 'static + Fn(String)
651    {
652        let js_params =
653            to_value(&params).map_err(|e| JsValue::from_str(&format!("serialize params: {e}")))?;
654        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
655            callback(v.as_string().unwrap_or_default());
656        });
657        Reflect::get(&self.inner, &"downloadFile".into())?
658            .dyn_into::<Function>()?
659            .call2(&self.inner, &js_params, cb.as_ref().unchecked_ref())?;
660        cb.forget();
661        Ok(())
662    }
663
664    /// Call `WebApp.requestEmojiStatusAccess(callback)`.
665    ///
666    /// # Examples
667    /// ```no_run
668    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
669    /// # let app = TelegramWebApp::instance().unwrap();
670    /// app.request_emoji_status_access(|granted| {
671    ///     let _ = granted;
672    /// })
673    /// .unwrap();
674    /// ```
675    ///
676    /// # Errors
677    /// Returns [`JsValue`] if the underlying JS call fails.
678    pub fn request_emoji_status_access<F>(&self, callback: F) -> Result<(), JsValue>
679    where
680        F: 'static + Fn(bool)
681    {
682        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
683            callback(v.as_bool().unwrap_or(false));
684        });
685        let f = Reflect::get(&self.inner, &"requestEmojiStatusAccess".into())?;
686        let func = f
687            .dyn_ref::<Function>()
688            .ok_or_else(|| JsValue::from_str("requestEmojiStatusAccess is not a function"))?;
689        func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
690        cb.forget();
691        Ok(())
692    }
693
694    /// Call `WebApp.setEmojiStatus(status, callback)`.
695    ///
696    /// # Examples
697    /// ```no_run
698    /// # use js_sys::Object;
699    /// # use js_sys::Reflect;
700    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
701    /// # let app = TelegramWebApp::instance().unwrap();
702    /// let status = Object::new();
703    /// let _ = Reflect::set(&status, &"custom_emoji_id".into(), &"123".into());
704    /// app.set_emoji_status(&status.into(), |success| {
705    ///     let _ = success;
706    /// })
707    /// .unwrap();
708    /// ```
709    ///
710    /// # Errors
711    /// Returns [`JsValue`] if the underlying JS call fails.
712    pub fn set_emoji_status<F>(&self, status: &JsValue, callback: F) -> Result<(), JsValue>
713    where
714        F: 'static + Fn(bool)
715    {
716        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
717            callback(v.as_bool().unwrap_or(false));
718        });
719        let f = Reflect::get(&self.inner, &"setEmojiStatus".into())?;
720        let func = f
721            .dyn_ref::<Function>()
722            .ok_or_else(|| JsValue::from_str("setEmojiStatus is not a function"))?;
723        func.call2(&self.inner, status, cb.as_ref().unchecked_ref())?;
724        cb.forget();
725        Ok(())
726    }
727
728    /// Call `WebApp.showPopup(params, callback)`.
729    ///
730    /// # Examples
731    /// ```no_run
732    /// # use js_sys::Object;
733    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
734    /// # let app = TelegramWebApp::instance().unwrap();
735    /// let params = Object::new();
736    /// app.show_popup(&params.into(), |id| {
737    ///     let _ = id;
738    /// })
739    /// .unwrap();
740    /// ```
741    pub fn show_popup<F>(&self, params: &JsValue, callback: F) -> Result<(), JsValue>
742    where
743        F: 'static + Fn(String)
744    {
745        let cb = Closure::<dyn FnMut(JsValue)>::new(move |id: JsValue| {
746            callback(id.as_string().unwrap_or_default());
747        });
748        Reflect::get(&self.inner, &"showPopup".into())?
749            .dyn_into::<Function>()?
750            .call2(&self.inner, params, cb.as_ref().unchecked_ref())?;
751        cb.forget();
752        Ok(())
753    }
754
755    /// Call `WebApp.showScanQrPopup(text, callback)`.
756    ///
757    /// # Examples
758    /// ```no_run
759    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
760    /// # let app = TelegramWebApp::instance().unwrap();
761    /// app.show_scan_qr_popup("Scan", |text| {
762    ///     let _ = text;
763    /// })
764    /// .unwrap();
765    /// ```
766    pub fn show_scan_qr_popup<F>(&self, text: &str, callback: F) -> Result<(), JsValue>
767    where
768        F: 'static + Fn(String)
769    {
770        let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
771            callback(value.as_string().unwrap_or_default());
772        });
773        Reflect::get(&self.inner, &"showScanQrPopup".into())?
774            .dyn_into::<Function>()?
775            .call2(&self.inner, &text.into(), cb.as_ref().unchecked_ref())?;
776        cb.forget();
777        Ok(())
778    }
779
780    /// Call `WebApp.closeScanQrPopup()`.
781    ///
782    /// # Examples
783    /// ```no_run
784    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
785    /// # let app = TelegramWebApp::instance().unwrap();
786    /// app.close_scan_qr_popup().unwrap();
787    /// ```
788    pub fn close_scan_qr_popup(&self) -> Result<(), JsValue> {
789        Reflect::get(&self.inner, &"closeScanQrPopup".into())?
790            .dyn_into::<Function>()?
791            .call0(&self.inner)?;
792        Ok(())
793    }
794
795    /// Call `WebApp.hideKeyboard()`.
796    fn bottom_button_object(&self, button: BottomButton) -> Result<Object, JsValue> {
797        let name = button.js_name();
798        Reflect::get(&self.inner, &name.into())
799            .inspect_err(|_| logger::error(&format!("{name} not available")))?
800            .dyn_into::<Object>()
801            .inspect_err(|_| logger::error(&format!("{name} is not an object")))
802    }
803
804    fn bottom_button_method(
805        &self,
806        button: BottomButton,
807        method: &str,
808        arg: Option<&JsValue>
809    ) -> Result<(), JsValue> {
810        let name = button.js_name();
811        let btn = self.bottom_button_object(button)?;
812        let f = Reflect::get(&btn, &method.into())
813            .inspect_err(|_| logger::error(&format!("{name}.{method} not available")))?;
814        let func = f.dyn_ref::<Function>().ok_or_else(|| {
815            logger::error(&format!("{name}.{method} is not a function"));
816            JsValue::from_str("not a function")
817        })?;
818        let result = match arg {
819            Some(v) => func.call1(&btn, v),
820            None => func.call0(&btn)
821        };
822        result.inspect_err(|_| logger::error(&format!("{name}.{method} call failed")))?;
823        Ok(())
824    }
825
826    /// Hide the on-screen keyboard.
827    /// Call `WebApp.hideKeyboard()`.
828    ///
829    /// # Examples
830    /// ```no_run
831    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
832    /// # let app = TelegramWebApp::instance().unwrap();
833    /// app.hide_keyboard().unwrap();
834    /// ```
835    ///
836    /// # Errors
837    /// Returns [`JsValue`] if the underlying JS call fails.
838    pub fn hide_keyboard(&self) -> Result<(), JsValue> {
839        self.call0("hideKeyboard")
840    }
841
842    /// Call `WebApp.readTextFromClipboard(callback)`.
843    ///
844    /// # Examples
845    /// ```no_run
846    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
847    /// # let app = TelegramWebApp::instance().unwrap();
848    /// app.read_text_from_clipboard(|text| {
849    ///     let _ = text;
850    /// })
851    /// .unwrap();
852    /// ```
853    ///
854    /// # Errors
855    /// Returns [`JsValue`] if the underlying JS call fails.
856    pub fn read_text_from_clipboard<F>(&self, callback: F) -> Result<(), JsValue>
857    where
858        F: 'static + Fn(String)
859    {
860        let cb = Closure::<dyn FnMut(JsValue)>::new(move |text: JsValue| {
861            callback(text.as_string().unwrap_or_default());
862        });
863        let f = Reflect::get(&self.inner, &"readTextFromClipboard".into())?;
864        let func = f
865            .dyn_ref::<Function>()
866            .ok_or_else(|| JsValue::from_str("readTextFromClipboard is not a function"))?;
867        func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
868        cb.forget();
869        Ok(())
870    }
871
872    /// Call `WebApp.MainButton.show()`.
873    ///
874    /// # Errors
875    /// Returns [`JsValue`] if the underlying JS call fails.
876    pub fn show_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
877        self.bottom_button_method(button, "show", None)
878    }
879
880    /// Hide a bottom button.
881    ///
882    /// # Errors
883    /// Returns [`JsValue`] if the underlying JS call fails.
884    pub fn hide_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
885        self.bottom_button_method(button, "hide", None)
886    }
887
888    /// Call `WebApp.ready()`.
889    ///
890    /// # Errors
891    /// Returns [`JsValue`] if the underlying JS call fails.
892    pub fn ready(&self) -> Result<(), JsValue> {
893        self.call0("ready")
894    }
895
896    /// Show back button.
897    ///
898    /// # Errors
899    /// Returns [`JsValue`] if the underlying JS call fails.
900    pub fn show_back_button(&self) -> Result<(), JsValue> {
901        self.call_nested0("BackButton", "show")
902    }
903
904    /// Hide back button.
905    ///
906    /// # Errors
907    /// Returns [`JsValue`] if the underlying JS call fails.
908    pub fn hide_back_button(&self) -> Result<(), JsValue> {
909        self.call_nested0("BackButton", "hide")
910    }
911
912    /// Call `WebApp.setHeaderColor(color)`.
913    ///
914    /// # Errors
915    /// Returns [`JsValue`] if the underlying JS call fails.
916    ///
917    /// # Examples
918    /// ```no_run
919    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
920    /// # let app = TelegramWebApp::instance().unwrap();
921    /// app.set_header_color("#ffffff").unwrap();
922    /// ```
923    pub fn set_header_color(&self, color: &str) -> Result<(), JsValue> {
924        self.call1("setHeaderColor", &color.into())
925    }
926
927    /// Call `WebApp.setBackgroundColor(color)`.
928    ///
929    /// # Errors
930    /// Returns [`JsValue`] if the underlying JS call fails.
931    ///
932    /// # Examples
933    /// ```no_run
934    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
935    /// # let app = TelegramWebApp::instance().unwrap();
936    /// app.set_background_color("#ffffff").unwrap();
937    /// ```
938    pub fn set_background_color(&self, color: &str) -> Result<(), JsValue> {
939        self.call1("setBackgroundColor", &color.into())
940    }
941
942    /// Call `WebApp.setBottomBarColor(color)`.
943    ///
944    /// # Errors
945    /// Returns [`JsValue`] if the underlying JS call fails.
946    ///
947    /// # Examples
948    /// ```no_run
949    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
950    /// # let app = TelegramWebApp::instance().unwrap();
951    /// app.set_bottom_bar_color("#ffffff").unwrap();
952    /// ```
953    pub fn set_bottom_bar_color(&self, color: &str) -> Result<(), JsValue> {
954        self.call1("setBottomBarColor", &color.into())
955    }
956
957    /// Set main button text.
958    ///
959    /// # Errors
960    /// Returns [`JsValue`] if the underlying JS call fails.
961    pub fn set_bottom_button_text(&self, button: BottomButton, text: &str) -> Result<(), JsValue> {
962        self.bottom_button_method(button, "setText", Some(&text.into()))
963    }
964
965    /// Set bottom button color (`setColor(color)`).
966    ///
967    /// # Errors
968    /// Returns [`JsValue`] if the underlying JS call fails.
969    ///
970    /// # Examples
971    /// ```no_run
972    /// # use telegram_webapp_sdk::webapp::{TelegramWebApp, BottomButton};
973    /// # let app = TelegramWebApp::instance().unwrap();
974    /// let _ = app.set_bottom_button_color(BottomButton::Main, "#ff0000");
975    /// ```
976    pub fn set_bottom_button_color(
977        &self,
978        button: BottomButton,
979        color: &str
980    ) -> Result<(), JsValue> {
981        self.bottom_button_method(button, "setColor", Some(&color.into()))
982    }
983
984    /// Set bottom button text color (`setTextColor(color)`).
985    ///
986    /// # Errors
987    /// Returns [`JsValue`] if the underlying JS call fails.
988    ///
989    /// # Examples
990    /// ```no_run
991    /// # use telegram_webapp_sdk::webapp::{TelegramWebApp, BottomButton};
992    /// # let app = TelegramWebApp::instance().unwrap();
993    /// let _ = app.set_bottom_button_text_color(BottomButton::Main, "#ffffff");
994    /// ```
995    pub fn set_bottom_button_text_color(
996        &self,
997        button: BottomButton,
998        color: &str
999    ) -> Result<(), JsValue> {
1000        self.bottom_button_method(button, "setTextColor", Some(&color.into()))
1001    }
1002
1003    /// Set callback for `onClick()` on a bottom button.
1004    ///
1005    /// Returns an [`EventHandle`] that can be used to remove the callback.
1006    ///
1007    /// # Errors
1008    /// Returns [`JsValue`] if the underlying JS call fails.
1009    pub fn set_bottom_button_callback<F>(
1010        &self,
1011        button: BottomButton,
1012        callback: F
1013    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1014    where
1015        F: 'static + Fn()
1016    {
1017        let btn_val = Reflect::get(&self.inner, &button.js_name().into())?;
1018        let btn = btn_val.dyn_into::<Object>()?;
1019        let cb = Closure::<dyn FnMut()>::new(callback);
1020        let f = Reflect::get(&btn, &"onClick".into())?;
1021        let func = f
1022            .dyn_ref::<Function>()
1023            .ok_or_else(|| JsValue::from_str("onClick is not a function"))?;
1024        func.call1(&btn, cb.as_ref().unchecked_ref())?;
1025        Ok(EventHandle::new(btn, "offClick", None, cb))
1026    }
1027
1028    /// Remove previously set bottom button callback.
1029    ///
1030    /// # Errors
1031    /// Returns [`JsValue`] if the underlying JS call fails.
1032    pub fn remove_bottom_button_callback(
1033        &self,
1034        handle: EventHandle<dyn FnMut()>
1035    ) -> Result<(), JsValue> {
1036        handle.unregister()
1037    }
1038
1039    /// Legacy alias for [`Self::show_bottom_button`] with
1040    /// [`BottomButton::Main`].
1041    pub fn show_main_button(&self) -> Result<(), JsValue> {
1042        self.show_bottom_button(BottomButton::Main)
1043    }
1044
1045    /// Show the secondary bottom button.
1046    pub fn show_secondary_button(&self) -> Result<(), JsValue> {
1047        self.show_bottom_button(BottomButton::Secondary)
1048    }
1049
1050    /// Legacy alias for [`Self::hide_bottom_button`] with
1051    /// [`BottomButton::Main`].
1052    pub fn hide_main_button(&self) -> Result<(), JsValue> {
1053        self.hide_bottom_button(BottomButton::Main)
1054    }
1055
1056    /// Hide the secondary bottom button.
1057    pub fn hide_secondary_button(&self) -> Result<(), JsValue> {
1058        self.hide_bottom_button(BottomButton::Secondary)
1059    }
1060
1061    /// Legacy alias for [`Self::set_bottom_button_text`] with
1062    /// [`BottomButton::Main`].
1063    pub fn set_main_button_text(&self, text: &str) -> Result<(), JsValue> {
1064        self.set_bottom_button_text(BottomButton::Main, text)
1065    }
1066
1067    /// Set text for the secondary bottom button.
1068    pub fn set_secondary_button_text(&self, text: &str) -> Result<(), JsValue> {
1069        self.set_bottom_button_text(BottomButton::Secondary, text)
1070    }
1071
1072    /// Legacy alias for [`Self::set_bottom_button_color`] with
1073    /// [`BottomButton::Main`].
1074    pub fn set_main_button_color(&self, color: &str) -> Result<(), JsValue> {
1075        self.set_bottom_button_color(BottomButton::Main, color)
1076    }
1077
1078    /// Set color for the secondary bottom button.
1079    pub fn set_secondary_button_color(&self, color: &str) -> Result<(), JsValue> {
1080        self.set_bottom_button_color(BottomButton::Secondary, color)
1081    }
1082
1083    /// Legacy alias for [`Self::set_bottom_button_text_color`] with
1084    /// [`BottomButton::Main`].
1085    pub fn set_main_button_text_color(&self, color: &str) -> Result<(), JsValue> {
1086        self.set_bottom_button_text_color(BottomButton::Main, color)
1087    }
1088
1089    /// Set text color for the secondary bottom button.
1090    pub fn set_secondary_button_text_color(&self, color: &str) -> Result<(), JsValue> {
1091        self.set_bottom_button_text_color(BottomButton::Secondary, color)
1092    }
1093
1094    /// Legacy alias for [`Self::set_bottom_button_callback`] with
1095    /// [`BottomButton::Main`].
1096    pub fn set_main_button_callback<F>(
1097        &self,
1098        callback: F
1099    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1100    where
1101        F: 'static + Fn()
1102    {
1103        self.set_bottom_button_callback(BottomButton::Main, callback)
1104    }
1105
1106    /// Set callback for the secondary bottom button.
1107    pub fn set_secondary_button_callback<F>(
1108        &self,
1109        callback: F
1110    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1111    where
1112        F: 'static + Fn()
1113    {
1114        self.set_bottom_button_callback(BottomButton::Secondary, callback)
1115    }
1116
1117    /// Legacy alias for [`Self::remove_bottom_button_callback`].
1118    pub fn remove_main_button_callback(
1119        &self,
1120        handle: EventHandle<dyn FnMut()>
1121    ) -> Result<(), JsValue> {
1122        self.remove_bottom_button_callback(handle)
1123    }
1124
1125    /// Remove callback for the secondary bottom button.
1126    pub fn remove_secondary_button_callback(
1127        &self,
1128        handle: EventHandle<dyn FnMut()>
1129    ) -> Result<(), JsValue> {
1130        self.remove_bottom_button_callback(handle)
1131    }
1132
1133    /// Register event handler (`web_app_event_name`, callback).
1134    ///
1135    /// Returns an [`EventHandle`] that can be passed to
1136    /// [`off_event`](Self::off_event).
1137    ///
1138    /// # Errors
1139    /// Returns [`JsValue`] if the underlying JS call fails.
1140    pub fn on_event<F>(
1141        &self,
1142        event: &str,
1143        callback: F
1144    ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
1145    where
1146        F: 'static + Fn(JsValue)
1147    {
1148        let cb = Closure::<dyn FnMut(JsValue)>::new(callback);
1149        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1150        let func = f
1151            .dyn_ref::<Function>()
1152            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1153        func.call2(&self.inner, &event.into(), cb.as_ref().unchecked_ref())?;
1154        Ok(EventHandle::new(
1155            self.inner.clone(),
1156            "offEvent",
1157            Some(event.to_owned()),
1158            cb
1159        ))
1160    }
1161
1162    /// Register a callback for a background event.
1163    ///
1164    /// Returns an [`EventHandle`] that can be passed to
1165    /// [`off_event`](Self::off_event).
1166    ///
1167    /// # Errors
1168    /// Returns [`JsValue`] if the underlying JS call fails.
1169    pub fn on_background_event<F>(
1170        &self,
1171        event: BackgroundEvent,
1172        callback: F
1173    ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
1174    where
1175        F: 'static + Fn(JsValue)
1176    {
1177        let cb = Closure::<dyn FnMut(JsValue)>::new(callback);
1178        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1179        let func = f
1180            .dyn_ref::<Function>()
1181            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1182        func.call2(
1183            &self.inner,
1184            &event.as_str().into(),
1185            cb.as_ref().unchecked_ref()
1186        )?;
1187        Ok(EventHandle::new(
1188            self.inner.clone(),
1189            "offEvent",
1190            Some(event.as_str().to_string()),
1191            cb
1192        ))
1193    }
1194
1195    /// Deregister a previously registered event handler.
1196    ///
1197    /// # Errors
1198    /// Returns [`JsValue`] if the underlying JS call fails.
1199    pub fn off_event<T: ?Sized>(&self, handle: EventHandle<T>) -> Result<(), JsValue> {
1200        handle.unregister()
1201    }
1202
1203    /// Internal: call `this[field][method]()`
1204    fn call_nested0(&self, field: &str, method: &str) -> Result<(), JsValue> {
1205        let obj = Reflect::get(&self.inner, &field.into())?;
1206        let f = Reflect::get(&obj, &method.into())?;
1207        let func = f
1208            .dyn_ref::<Function>()
1209            .ok_or_else(|| JsValue::from_str("not a function"))?;
1210        func.call0(&obj)?;
1211        Ok(())
1212    }
1213
1214    // === Internal generic method helpers ===
1215
1216    fn call0(&self, method: &str) -> Result<(), JsValue> {
1217        let f = Reflect::get(&self.inner, &method.into())?;
1218        let func = f
1219            .dyn_ref::<Function>()
1220            .ok_or_else(|| JsValue::from_str("not a function"))?;
1221        func.call0(&self.inner)?;
1222        Ok(())
1223    }
1224
1225    fn call1(&self, method: &str, arg: &JsValue) -> Result<(), JsValue> {
1226        let f = Reflect::get(&self.inner, &method.into())?;
1227        let func = f
1228            .dyn_ref::<Function>()
1229            .ok_or_else(|| JsValue::from_str("not a function"))?;
1230        func.call1(&self.inner, arg)?;
1231        Ok(())
1232    }
1233
1234    /// Returns the current viewport height in pixels.
1235    ///
1236    /// # Examples
1237    /// ```no_run
1238    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1239    /// # let app = TelegramWebApp::instance().unwrap();
1240    /// let _ = app.viewport_height();
1241    /// ```
1242    pub fn viewport_height(&self) -> Option<f64> {
1243        Reflect::get(&self.inner, &"viewportHeight".into())
1244            .ok()?
1245            .as_f64()
1246    }
1247
1248    /// Returns the current viewport width in pixels.
1249    ///
1250    /// # Examples
1251    /// ```no_run
1252    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1253    /// # let app = TelegramWebApp::instance().unwrap();
1254    /// let _ = app.viewport_width();
1255    /// ```
1256    pub fn viewport_width(&self) -> Option<f64> {
1257        Reflect::get(&self.inner, &"viewportWidth".into())
1258            .ok()?
1259            .as_f64()
1260    }
1261
1262    /// Returns the stable viewport height in pixels.
1263    ///
1264    /// # Examples
1265    /// ```no_run
1266    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1267    /// # let app = TelegramWebApp::instance().unwrap();
1268    /// let _ = app.viewport_stable_height();
1269    /// ```
1270    pub fn viewport_stable_height(&self) -> Option<f64> {
1271        Reflect::get(&self.inner, &"viewportStableHeight".into())
1272            .ok()?
1273            .as_f64()
1274    }
1275
1276    pub fn is_expanded(&self) -> bool {
1277        Reflect::get(&self.inner, &"isExpanded".into())
1278            .ok()
1279            .and_then(|v| v.as_bool())
1280            .unwrap_or(false)
1281    }
1282
1283    /// Call `WebApp.expand()` to expand the viewport.
1284    ///
1285    /// # Errors
1286    /// Returns [`JsValue`] if the underlying JS call fails.
1287    pub fn expand_viewport(&self) -> Result<(), JsValue> {
1288        self.call0("expand")
1289    }
1290
1291    /// Register a callback for theme changes.
1292    ///
1293    /// Returns an [`EventHandle`] that can be passed to
1294    /// [`off_event`](Self::off_event).
1295    ///
1296    /// # Errors
1297    /// Returns [`JsValue`] if the underlying JS call fails.
1298    pub fn on_theme_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
1299    where
1300        F: 'static + Fn()
1301    {
1302        let cb = Closure::<dyn FnMut()>::new(callback);
1303        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1304        let func = f
1305            .dyn_ref::<Function>()
1306            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1307        func.call2(
1308            &self.inner,
1309            &"themeChanged".into(),
1310            cb.as_ref().unchecked_ref()
1311        )?;
1312        Ok(EventHandle::new(
1313            self.inner.clone(),
1314            "offEvent",
1315            Some("themeChanged".to_string()),
1316            cb
1317        ))
1318    }
1319
1320    /// Register a callback for safe area changes.
1321    ///
1322    /// Returns an [`EventHandle`] that can be passed to
1323    /// [`off_event`](Self::off_event).
1324    ///
1325    /// # Errors
1326    /// Returns [`JsValue`] if the underlying JS call fails.
1327    pub fn on_safe_area_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
1328    where
1329        F: 'static + Fn()
1330    {
1331        let cb = Closure::<dyn FnMut()>::new(callback);
1332        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1333        let func = f
1334            .dyn_ref::<Function>()
1335            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1336        func.call2(
1337            &self.inner,
1338            &"safeAreaChanged".into(),
1339            cb.as_ref().unchecked_ref()
1340        )?;
1341        Ok(EventHandle::new(
1342            self.inner.clone(),
1343            "offEvent",
1344            Some("safeAreaChanged".to_string()),
1345            cb
1346        ))
1347    }
1348
1349    /// Register a callback for content safe area changes.
1350    ///
1351    /// Returns an [`EventHandle`] that can be passed to
1352    /// [`off_event`](Self::off_event).
1353    ///
1354    /// # Errors
1355    /// Returns [`JsValue`] if the underlying JS call fails.
1356    pub fn on_content_safe_area_changed<F>(
1357        &self,
1358        callback: F
1359    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1360    where
1361        F: 'static + Fn()
1362    {
1363        let cb = Closure::<dyn FnMut()>::new(callback);
1364        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1365        let func = f
1366            .dyn_ref::<Function>()
1367            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1368        func.call2(
1369            &self.inner,
1370            &"contentSafeAreaChanged".into(),
1371            cb.as_ref().unchecked_ref()
1372        )?;
1373        Ok(EventHandle::new(
1374            self.inner.clone(),
1375            "offEvent",
1376            Some("contentSafeAreaChanged".to_string()),
1377            cb
1378        ))
1379    }
1380
1381    /// Register a callback for viewport changes.
1382    ///
1383    /// Returns an [`EventHandle`] that can be passed to
1384    /// [`off_event`](Self::off_event).
1385    ///
1386    /// # Errors
1387    /// Returns [`JsValue`] if the underlying JS call fails.
1388    pub fn on_viewport_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
1389    where
1390        F: 'static + Fn()
1391    {
1392        let cb = Closure::<dyn FnMut()>::new(callback);
1393        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1394        let func = f
1395            .dyn_ref::<Function>()
1396            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1397        func.call2(
1398            &self.inner,
1399            &"viewportChanged".into(),
1400            cb.as_ref().unchecked_ref()
1401        )?;
1402        Ok(EventHandle::new(
1403            self.inner.clone(),
1404            "offEvent",
1405            Some("viewportChanged".to_string()),
1406            cb
1407        ))
1408    }
1409
1410    /// Register a callback for received clipboard text.
1411    ///
1412    /// Returns an [`EventHandle`] that can be passed to
1413    /// [`off_event`](Self::off_event).
1414    ///
1415    /// # Errors
1416    /// Returns [`JsValue`] if the underlying JS call fails.
1417    pub fn on_clipboard_text_received<F>(
1418        &self,
1419        callback: F
1420    ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
1421    where
1422        F: 'static + Fn(String)
1423    {
1424        let cb = Closure::<dyn FnMut(JsValue)>::new(move |text: JsValue| {
1425            callback(text.as_string().unwrap_or_default());
1426        });
1427        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1428        let func = f
1429            .dyn_ref::<Function>()
1430            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1431        func.call2(
1432            &self.inner,
1433            &"clipboardTextReceived".into(),
1434            cb.as_ref().unchecked_ref()
1435        )?;
1436        Ok(EventHandle::new(
1437            self.inner.clone(),
1438            "offEvent",
1439            Some("clipboardTextReceived".to_string()),
1440            cb
1441        ))
1442    }
1443
1444    /// Register a callback for invoice payment result.
1445    ///
1446    /// Returns an [`EventHandle`] that can be passed to
1447    /// [`off_event`](Self::off_event).
1448    ///
1449    /// # Examples
1450    /// ```no_run
1451    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1452    /// # let app = TelegramWebApp::instance().unwrap();
1453    /// let handle = app
1454    ///     .on_invoice_closed(|status| {
1455    ///         let _ = status;
1456    ///     })
1457    ///     .unwrap();
1458    /// app.off_event(handle).unwrap();
1459    /// ```
1460    ///
1461    /// # Errors
1462    /// Returns [`JsValue`] if the underlying JS call fails.
1463    pub fn on_invoice_closed<F>(
1464        &self,
1465        callback: F
1466    ) -> Result<EventHandle<dyn FnMut(String)>, JsValue>
1467    where
1468        F: 'static + Fn(String)
1469    {
1470        let cb = Closure::<dyn FnMut(String)>::new(callback);
1471        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1472        let func = f
1473            .dyn_ref::<Function>()
1474            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1475        func.call2(
1476            &self.inner,
1477            &"invoiceClosed".into(),
1478            cb.as_ref().unchecked_ref()
1479        )?;
1480        Ok(EventHandle::new(
1481            self.inner.clone(),
1482            "offEvent",
1483            Some("invoiceClosed".to_string()),
1484            cb
1485        ))
1486    }
1487
1488    /// Registers a callback for the native back button.
1489    ///
1490    /// Returns an [`EventHandle`] that can be passed to
1491    /// [`remove_back_button_callback`](Self::remove_back_button_callback).
1492    ///
1493    /// # Examples
1494    /// ```no_run
1495    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1496    /// # let app = TelegramWebApp::instance().unwrap();
1497    /// let handle = app.set_back_button_callback(|| {}).expect("callback");
1498    /// app.remove_back_button_callback(handle).unwrap();
1499    /// ```
1500    ///
1501    /// # Errors
1502    /// Returns [`JsValue`] if the underlying JS call fails.
1503    pub fn set_back_button_callback<F>(
1504        &self,
1505        callback: F
1506    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1507    where
1508        F: 'static + Fn()
1509    {
1510        let back_button_val = Reflect::get(&self.inner, &"BackButton".into())?;
1511        let back_button = back_button_val.dyn_into::<Object>()?;
1512        let cb = Closure::<dyn FnMut()>::new(callback);
1513        let f = Reflect::get(&back_button, &"onClick".into())?;
1514        let func = f
1515            .dyn_ref::<Function>()
1516            .ok_or_else(|| JsValue::from_str("onClick is not a function"))?;
1517        func.call1(&back_button, cb.as_ref().unchecked_ref())?;
1518        Ok(EventHandle::new(back_button, "offClick", None, cb))
1519    }
1520
1521    /// Remove previously set back button callback.
1522    ///
1523    /// # Errors
1524    /// Returns [`JsValue`] if the underlying JS call fails.
1525    pub fn remove_back_button_callback(
1526        &self,
1527        handle: EventHandle<dyn FnMut()>
1528    ) -> Result<(), JsValue> {
1529        handle.unregister()
1530    }
1531    /// Returns whether the native back button is visible.
1532    ///
1533    /// # Examples
1534    /// ```no_run
1535    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1536    /// # let app = TelegramWebApp::instance().unwrap();
1537    /// let _ = app.is_back_button_visible();
1538    /// ```
1539    pub fn is_back_button_visible(&self) -> bool {
1540        Reflect::get(&self.inner, &"BackButton".into())
1541            .ok()
1542            .and_then(|bb| Reflect::get(&bb, &"isVisible".into()).ok())
1543            .and_then(|v| v.as_bool())
1544            .unwrap_or(false)
1545    }
1546}
1547
1548#[cfg(test)]
1549mod tests {
1550    use std::{
1551        cell::{Cell, RefCell},
1552        rc::Rc
1553    };
1554
1555    use js_sys::{Function, Object, Reflect};
1556    use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
1557    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
1558    use web_sys::window;
1559
1560    use super::*;
1561
1562    wasm_bindgen_test_configure!(run_in_browser);
1563
1564    #[allow(dead_code)]
1565    fn setup_webapp() -> Object {
1566        let win = window().unwrap();
1567        let telegram = Object::new();
1568        let webapp = Object::new();
1569        let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
1570        let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
1571        webapp
1572    }
1573
1574    #[wasm_bindgen_test]
1575    #[allow(dead_code, clippy::unused_unit)]
1576    fn hide_keyboard_calls_js() {
1577        let webapp = setup_webapp();
1578        let called = Rc::new(Cell::new(false));
1579        let called_clone = Rc::clone(&called);
1580
1581        let hide_cb = Closure::<dyn FnMut()>::new(move || {
1582            called_clone.set(true);
1583        });
1584        let _ = Reflect::set(
1585            &webapp,
1586            &"hideKeyboard".into(),
1587            hide_cb.as_ref().unchecked_ref()
1588        );
1589        hide_cb.forget();
1590
1591        let app = TelegramWebApp::instance().unwrap();
1592        app.hide_keyboard().unwrap();
1593        assert!(called.get());
1594    }
1595
1596    #[wasm_bindgen_test]
1597    #[allow(dead_code, clippy::unused_unit)]
1598    fn hide_main_button_calls_js() {
1599        let webapp = setup_webapp();
1600        let main_button = Object::new();
1601        let called = Rc::new(Cell::new(false));
1602        let called_clone = Rc::clone(&called);
1603
1604        let hide_cb = Closure::<dyn FnMut()>::new(move || {
1605            called_clone.set(true);
1606        });
1607        let _ = Reflect::set(
1608            &main_button,
1609            &"hide".into(),
1610            hide_cb.as_ref().unchecked_ref()
1611        );
1612        hide_cb.forget();
1613
1614        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
1615
1616        let app = TelegramWebApp::instance().unwrap();
1617        app.hide_bottom_button(BottomButton::Main).unwrap();
1618        assert!(called.get());
1619    }
1620
1621    #[wasm_bindgen_test]
1622    #[allow(dead_code, clippy::unused_unit)]
1623    fn hide_secondary_button_calls_js() {
1624        let webapp = setup_webapp();
1625        let secondary_button = Object::new();
1626        let called = Rc::new(Cell::new(false));
1627        let called_clone = Rc::clone(&called);
1628
1629        let hide_cb = Closure::<dyn FnMut()>::new(move || {
1630            called_clone.set(true);
1631        });
1632        let _ = Reflect::set(
1633            &secondary_button,
1634            &"hide".into(),
1635            hide_cb.as_ref().unchecked_ref()
1636        );
1637        hide_cb.forget();
1638
1639        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
1640
1641        let app = TelegramWebApp::instance().unwrap();
1642        app.hide_bottom_button(BottomButton::Secondary).unwrap();
1643        assert!(called.get());
1644    }
1645
1646    #[wasm_bindgen_test]
1647    #[allow(dead_code, clippy::unused_unit)]
1648    fn set_bottom_button_color_calls_js() {
1649        let webapp = setup_webapp();
1650        let main_button = Object::new();
1651        let received = Rc::new(RefCell::new(None));
1652        let rc_clone = Rc::clone(&received);
1653
1654        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1655            *rc_clone.borrow_mut() = v.as_string();
1656        });
1657        let _ = Reflect::set(
1658            &main_button,
1659            &"setColor".into(),
1660            set_color_cb.as_ref().unchecked_ref()
1661        );
1662        set_color_cb.forget();
1663
1664        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
1665
1666        let app = TelegramWebApp::instance().unwrap();
1667        app.set_bottom_button_color(BottomButton::Main, "#00ff00")
1668            .unwrap();
1669        assert_eq!(received.borrow().as_deref(), Some("#00ff00"));
1670    }
1671
1672    #[wasm_bindgen_test]
1673    #[allow(dead_code, clippy::unused_unit)]
1674    fn set_secondary_button_color_calls_js() {
1675        let webapp = setup_webapp();
1676        let secondary_button = Object::new();
1677        let received = Rc::new(RefCell::new(None));
1678        let rc_clone = Rc::clone(&received);
1679
1680        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1681            *rc_clone.borrow_mut() = v.as_string();
1682        });
1683        let _ = Reflect::set(
1684            &secondary_button,
1685            &"setColor".into(),
1686            set_color_cb.as_ref().unchecked_ref()
1687        );
1688        set_color_cb.forget();
1689
1690        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
1691
1692        let app = TelegramWebApp::instance().unwrap();
1693        app.set_bottom_button_color(BottomButton::Secondary, "#00ff00")
1694            .unwrap();
1695        assert_eq!(received.borrow().as_deref(), Some("#00ff00"));
1696    }
1697
1698    #[wasm_bindgen_test]
1699    #[allow(dead_code, clippy::unused_unit)]
1700    fn set_bottom_button_text_color_calls_js() {
1701        let webapp = setup_webapp();
1702        let main_button = Object::new();
1703        let received = Rc::new(RefCell::new(None));
1704        let rc_clone = Rc::clone(&received);
1705
1706        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1707            *rc_clone.borrow_mut() = v.as_string();
1708        });
1709        let _ = Reflect::set(
1710            &main_button,
1711            &"setTextColor".into(),
1712            set_color_cb.as_ref().unchecked_ref()
1713        );
1714        set_color_cb.forget();
1715
1716        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
1717
1718        let app = TelegramWebApp::instance().unwrap();
1719        app.set_bottom_button_text_color(BottomButton::Main, "#112233")
1720            .unwrap();
1721        assert_eq!(received.borrow().as_deref(), Some("#112233"));
1722    }
1723
1724    #[wasm_bindgen_test]
1725    #[allow(dead_code, clippy::unused_unit)]
1726    fn set_secondary_button_text_color_calls_js() {
1727        let webapp = setup_webapp();
1728        let secondary_button = Object::new();
1729        let received = Rc::new(RefCell::new(None));
1730        let rc_clone = Rc::clone(&received);
1731
1732        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1733            *rc_clone.borrow_mut() = v.as_string();
1734        });
1735        let _ = Reflect::set(
1736            &secondary_button,
1737            &"setTextColor".into(),
1738            set_color_cb.as_ref().unchecked_ref()
1739        );
1740        set_color_cb.forget();
1741
1742        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
1743
1744        let app = TelegramWebApp::instance().unwrap();
1745        app.set_bottom_button_text_color(BottomButton::Secondary, "#112233")
1746            .unwrap();
1747        assert_eq!(received.borrow().as_deref(), Some("#112233"));
1748    }
1749
1750    #[wasm_bindgen_test]
1751    #[allow(dead_code, clippy::unused_unit)]
1752    fn set_header_color_calls_js() {
1753        let webapp = setup_webapp();
1754        let received = Rc::new(RefCell::new(None));
1755        let rc_clone = Rc::clone(&received);
1756
1757        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1758            *rc_clone.borrow_mut() = v.as_string();
1759        });
1760        let _ = Reflect::set(
1761            &webapp,
1762            &"setHeaderColor".into(),
1763            cb.as_ref().unchecked_ref()
1764        );
1765        cb.forget();
1766
1767        let app = TelegramWebApp::instance().unwrap();
1768        app.set_header_color("#abcdef").unwrap();
1769        assert_eq!(received.borrow().as_deref(), Some("#abcdef"));
1770    }
1771
1772    #[wasm_bindgen_test]
1773    #[allow(dead_code, clippy::unused_unit)]
1774    fn set_background_color_calls_js() {
1775        let webapp = setup_webapp();
1776        let received = Rc::new(RefCell::new(None));
1777        let rc_clone = Rc::clone(&received);
1778
1779        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1780            *rc_clone.borrow_mut() = v.as_string();
1781        });
1782        let _ = Reflect::set(
1783            &webapp,
1784            &"setBackgroundColor".into(),
1785            cb.as_ref().unchecked_ref()
1786        );
1787        cb.forget();
1788
1789        let app = TelegramWebApp::instance().unwrap();
1790        app.set_background_color("#123456").unwrap();
1791        assert_eq!(received.borrow().as_deref(), Some("#123456"));
1792    }
1793
1794    #[wasm_bindgen_test]
1795    #[allow(dead_code, clippy::unused_unit)]
1796    fn set_bottom_bar_color_calls_js() {
1797        let webapp = setup_webapp();
1798        let received = Rc::new(RefCell::new(None));
1799        let rc_clone = Rc::clone(&received);
1800
1801        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1802            *rc_clone.borrow_mut() = v.as_string();
1803        });
1804        let _ = Reflect::set(
1805            &webapp,
1806            &"setBottomBarColor".into(),
1807            cb.as_ref().unchecked_ref()
1808        );
1809        cb.forget();
1810
1811        let app = TelegramWebApp::instance().unwrap();
1812        app.set_bottom_bar_color("#654321").unwrap();
1813        assert_eq!(received.borrow().as_deref(), Some("#654321"));
1814    }
1815
1816    #[wasm_bindgen_test]
1817    #[allow(dead_code, clippy::unused_unit)]
1818    fn viewport_dimensions() {
1819        let webapp = setup_webapp();
1820        let _ = Reflect::set(&webapp, &"viewportWidth".into(), &JsValue::from_f64(320.0));
1821        let _ = Reflect::set(
1822            &webapp,
1823            &"viewportStableHeight".into(),
1824            &JsValue::from_f64(480.0)
1825        );
1826        let app = TelegramWebApp::instance().unwrap();
1827        assert_eq!(app.viewport_width(), Some(320.0));
1828        assert_eq!(app.viewport_stable_height(), Some(480.0));
1829    }
1830
1831    #[wasm_bindgen_test]
1832    #[allow(dead_code, clippy::unused_unit)]
1833    fn back_button_visibility_and_callback() {
1834        let webapp = setup_webapp();
1835        let back_button = Object::new();
1836        let _ = Reflect::set(&webapp, &"BackButton".into(), &back_button);
1837        let _ = Reflect::set(&back_button, &"isVisible".into(), &JsValue::TRUE);
1838
1839        let on_click = Function::new_with_args("cb", "this.cb = cb;");
1840        let off_click = Function::new_with_args("", "delete this.cb;");
1841        let _ = Reflect::set(&back_button, &"onClick".into(), &on_click);
1842        let _ = Reflect::set(&back_button, &"offClick".into(), &off_click);
1843
1844        let called = Rc::new(Cell::new(false));
1845        let called_clone = Rc::clone(&called);
1846
1847        let app = TelegramWebApp::instance().unwrap();
1848        assert!(app.is_back_button_visible());
1849        let handle = app
1850            .set_back_button_callback(move || {
1851                called_clone.set(true);
1852            })
1853            .unwrap();
1854
1855        let stored = Reflect::has(&back_button, &"cb".into()).unwrap();
1856        assert!(stored);
1857
1858        let cb_fn = Reflect::get(&back_button, &"cb".into())
1859            .unwrap()
1860            .dyn_into::<Function>()
1861            .unwrap();
1862        let _ = cb_fn.call0(&JsValue::NULL);
1863        assert!(called.get());
1864
1865        app.remove_back_button_callback(handle).unwrap();
1866        let stored_after = Reflect::has(&back_button, &"cb".into()).unwrap();
1867        assert!(!stored_after);
1868    }
1869
1870    #[wasm_bindgen_test]
1871    #[allow(dead_code, clippy::unused_unit)]
1872    fn bottom_button_callback_register_and_remove() {
1873        let webapp = setup_webapp();
1874        let main_button = Object::new();
1875        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
1876
1877        let on_click = Function::new_with_args("cb", "this.cb = cb;");
1878        let off_click = Function::new_with_args("", "delete this.cb;");
1879        let _ = Reflect::set(&main_button, &"onClick".into(), &on_click);
1880        let _ = Reflect::set(&main_button, &"offClick".into(), &off_click);
1881
1882        let called = Rc::new(Cell::new(false));
1883        let called_clone = Rc::clone(&called);
1884
1885        let app = TelegramWebApp::instance().unwrap();
1886        let handle = app
1887            .set_bottom_button_callback(BottomButton::Main, move || {
1888                called_clone.set(true);
1889            })
1890            .unwrap();
1891
1892        let stored = Reflect::has(&main_button, &"cb".into()).unwrap();
1893        assert!(stored);
1894
1895        let cb_fn = Reflect::get(&main_button, &"cb".into())
1896            .unwrap()
1897            .dyn_into::<Function>()
1898            .unwrap();
1899        let _ = cb_fn.call0(&JsValue::NULL);
1900        assert!(called.get());
1901
1902        app.remove_bottom_button_callback(handle).unwrap();
1903        let stored_after = Reflect::has(&main_button, &"cb".into()).unwrap();
1904        assert!(!stored_after);
1905    }
1906
1907    #[wasm_bindgen_test]
1908    #[allow(dead_code, clippy::unused_unit)]
1909    fn secondary_button_callback_register_and_remove() {
1910        let webapp = setup_webapp();
1911        let secondary_button = Object::new();
1912        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
1913
1914        let on_click = Function::new_with_args("cb", "this.cb = cb;");
1915        let off_click = Function::new_with_args("", "delete this.cb;");
1916        let _ = Reflect::set(&secondary_button, &"onClick".into(), &on_click);
1917        let _ = Reflect::set(&secondary_button, &"offClick".into(), &off_click);
1918
1919        let called = Rc::new(Cell::new(false));
1920        let called_clone = Rc::clone(&called);
1921
1922        let app = TelegramWebApp::instance().unwrap();
1923        let handle = app
1924            .set_bottom_button_callback(BottomButton::Secondary, move || {
1925                called_clone.set(true);
1926            })
1927            .unwrap();
1928
1929        let stored = Reflect::has(&secondary_button, &"cb".into()).unwrap();
1930        assert!(stored);
1931
1932        let cb_fn = Reflect::get(&secondary_button, &"cb".into())
1933            .unwrap()
1934            .dyn_into::<Function>()
1935            .unwrap();
1936        let _ = cb_fn.call0(&JsValue::NULL);
1937        assert!(called.get());
1938
1939        app.remove_bottom_button_callback(handle).unwrap();
1940        let stored_after = Reflect::has(&secondary_button, &"cb".into()).unwrap();
1941        assert!(!stored_after);
1942    }
1943
1944    #[wasm_bindgen_test]
1945    #[allow(dead_code, clippy::unused_unit)]
1946    fn on_event_register_and_remove() {
1947        let webapp = setup_webapp();
1948        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
1949        let off_event = Function::new_with_args("name", "delete this[name];");
1950        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
1951        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
1952
1953        let app = TelegramWebApp::instance().unwrap();
1954        let handle = app.on_event("test", |_: JsValue| {}).unwrap();
1955        assert!(Reflect::has(&webapp, &"test".into()).unwrap());
1956        app.off_event(handle).unwrap();
1957        assert!(!Reflect::has(&webapp, &"test".into()).unwrap());
1958    }
1959
1960    #[wasm_bindgen_test]
1961    #[allow(dead_code, clippy::unused_unit)]
1962    fn background_event_register_and_remove() {
1963        let webapp = setup_webapp();
1964        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
1965        let off_event = Function::new_with_args("name", "delete this[name];");
1966        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
1967        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
1968
1969        let app = TelegramWebApp::instance().unwrap();
1970        let handle = app
1971            .on_background_event(BackgroundEvent::MainButtonClicked, |_| {})
1972            .unwrap();
1973        assert!(Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
1974        app.off_event(handle).unwrap();
1975        assert!(!Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
1976    }
1977
1978    #[wasm_bindgen_test]
1979    #[allow(dead_code, clippy::unused_unit)]
1980    fn background_event_delivers_data() {
1981        let webapp = setup_webapp();
1982        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
1983        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
1984
1985        let app = TelegramWebApp::instance().unwrap();
1986        let received = Rc::new(RefCell::new(String::new()));
1987        let received_clone = Rc::clone(&received);
1988        let _handle = app
1989            .on_background_event(BackgroundEvent::InvoiceClosed, move |v| {
1990                *received_clone.borrow_mut() = v.as_string().unwrap_or_default();
1991            })
1992            .unwrap();
1993
1994        let cb = Reflect::get(&webapp, &"invoiceClosed".into())
1995            .unwrap()
1996            .dyn_into::<Function>()
1997            .unwrap();
1998        let _ = cb.call1(&JsValue::NULL, &JsValue::from_str("paid"));
1999        assert_eq!(received.borrow().as_str(), "paid");
2000    }
2001
2002    #[wasm_bindgen_test]
2003    #[allow(dead_code, clippy::unused_unit)]
2004    fn theme_changed_register_and_remove() {
2005        let webapp = setup_webapp();
2006        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2007        let off_event = Function::new_with_args("name", "delete this[name];");
2008        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2009        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2010
2011        let app = TelegramWebApp::instance().unwrap();
2012        let handle = app.on_theme_changed(|| {}).unwrap();
2013        assert!(Reflect::has(&webapp, &"themeChanged".into()).unwrap());
2014        app.off_event(handle).unwrap();
2015        assert!(!Reflect::has(&webapp, &"themeChanged".into()).unwrap());
2016    }
2017
2018    #[wasm_bindgen_test]
2019    #[allow(dead_code, clippy::unused_unit)]
2020    fn safe_area_changed_register_and_remove() {
2021        let webapp = setup_webapp();
2022        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2023        let off_event = Function::new_with_args("name", "delete this[name];");
2024        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2025        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2026
2027        let app = TelegramWebApp::instance().unwrap();
2028        let handle = app.on_safe_area_changed(|| {}).unwrap();
2029        assert!(Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
2030        app.off_event(handle).unwrap();
2031        assert!(!Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
2032    }
2033
2034    #[wasm_bindgen_test]
2035    #[allow(dead_code, clippy::unused_unit)]
2036    fn content_safe_area_changed_register_and_remove() {
2037        let webapp = setup_webapp();
2038        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2039        let off_event = Function::new_with_args("name", "delete this[name];");
2040        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2041        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2042
2043        let app = TelegramWebApp::instance().unwrap();
2044        let handle = app.on_content_safe_area_changed(|| {}).unwrap();
2045        assert!(Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
2046        app.off_event(handle).unwrap();
2047        assert!(!Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
2048    }
2049
2050    #[wasm_bindgen_test]
2051    #[allow(dead_code, clippy::unused_unit)]
2052    fn viewport_changed_register_and_remove() {
2053        let webapp = setup_webapp();
2054        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2055        let off_event = Function::new_with_args("name", "delete this[name];");
2056        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2057        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2058
2059        let app = TelegramWebApp::instance().unwrap();
2060        let handle = app.on_viewport_changed(|| {}).unwrap();
2061        assert!(Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
2062        app.off_event(handle).unwrap();
2063        assert!(!Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
2064    }
2065
2066    #[wasm_bindgen_test]
2067    #[allow(dead_code, clippy::unused_unit)]
2068    fn clipboard_text_received_register_and_remove() {
2069        let webapp = setup_webapp();
2070        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2071        let off_event = Function::new_with_args("name", "delete this[name];");
2072        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2073        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2074
2075        let app = TelegramWebApp::instance().unwrap();
2076        let handle = app.on_clipboard_text_received(|_| {}).unwrap();
2077        assert!(Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
2078        app.off_event(handle).unwrap();
2079        assert!(!Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
2080    }
2081
2082    #[wasm_bindgen_test]
2083    #[allow(dead_code, clippy::unused_unit)]
2084    fn open_link_and_telegram_link() {
2085        let webapp = setup_webapp();
2086        let open_link = Function::new_with_args("url", "this.open_link = url;");
2087        let open_tg_link = Function::new_with_args("url", "this.open_tg_link = url;");
2088        let _ = Reflect::set(&webapp, &"openLink".into(), &open_link);
2089        let _ = Reflect::set(&webapp, &"openTelegramLink".into(), &open_tg_link);
2090
2091        let app = TelegramWebApp::instance().unwrap();
2092        let url = "https://example.com";
2093        app.open_link(url).unwrap();
2094        app.open_telegram_link(url).unwrap();
2095
2096        assert_eq!(
2097            Reflect::get(&webapp, &"open_link".into())
2098                .unwrap()
2099                .as_string()
2100                .as_deref(),
2101            Some(url)
2102        );
2103        assert_eq!(
2104            Reflect::get(&webapp, &"open_tg_link".into())
2105                .unwrap()
2106                .as_string()
2107                .as_deref(),
2108            Some(url)
2109        );
2110    }
2111
2112    #[wasm_bindgen_test]
2113    #[allow(dead_code, clippy::unused_unit)]
2114    fn invoice_closed_register_and_remove() {
2115        let webapp = setup_webapp();
2116        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2117        let off_event = Function::new_with_args("name", "delete this[name];");
2118        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2119        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2120
2121        let app = TelegramWebApp::instance().unwrap();
2122        let handle = app.on_invoice_closed(|_| {}).unwrap();
2123        assert!(Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
2124        app.off_event(handle).unwrap();
2125        assert!(!Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
2126    }
2127
2128    #[wasm_bindgen_test]
2129    #[allow(dead_code, clippy::unused_unit)]
2130    fn invoice_closed_invokes_callback() {
2131        let webapp = setup_webapp();
2132        let on_event = Function::new_with_args("name, cb", "this.cb = cb;");
2133        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2134
2135        let app = TelegramWebApp::instance().unwrap();
2136        let status = Rc::new(RefCell::new(String::new()));
2137        let status_clone = Rc::clone(&status);
2138        app.on_invoice_closed(move |s| {
2139            *status_clone.borrow_mut() = s;
2140        })
2141        .unwrap();
2142
2143        let cb = Reflect::get(&webapp, &"cb".into())
2144            .unwrap()
2145            .dyn_into::<Function>()
2146            .unwrap();
2147        cb.call1(&webapp, &"paid".into()).unwrap();
2148        assert_eq!(status.borrow().as_str(), "paid");
2149        cb.call1(&webapp, &"failed".into()).unwrap();
2150        assert_eq!(status.borrow().as_str(), "failed");
2151    }
2152
2153    #[wasm_bindgen_test]
2154    #[allow(dead_code, clippy::unused_unit)]
2155    fn open_invoice_invokes_callback() {
2156        let webapp = setup_webapp();
2157        let open_invoice = Function::new_with_args("url, cb", "cb('paid');");
2158        let _ = Reflect::set(&webapp, &"openInvoice".into(), &open_invoice);
2159
2160        let app = TelegramWebApp::instance().unwrap();
2161        let status = Rc::new(RefCell::new(String::new()));
2162        let status_clone = Rc::clone(&status);
2163
2164        app.open_invoice("https://invoice", move |s| {
2165            *status_clone.borrow_mut() = s;
2166        })
2167        .unwrap();
2168
2169        assert_eq!(status.borrow().as_str(), "paid");
2170    }
2171
2172    #[wasm_bindgen_test]
2173    #[allow(dead_code, clippy::unused_unit)]
2174    fn switch_inline_query_calls_js() {
2175        let webapp = setup_webapp();
2176        let switch_inline =
2177            Function::new_with_args("query, types", "this.query = query; this.types = types;");
2178        let _ = Reflect::set(&webapp, &"switchInlineQuery".into(), &switch_inline);
2179
2180        let app = TelegramWebApp::instance().unwrap();
2181        let types = JsValue::from_str("users");
2182        app.switch_inline_query("search", Some(&types)).unwrap();
2183
2184        assert_eq!(
2185            Reflect::get(&webapp, &"query".into())
2186                .unwrap()
2187                .as_string()
2188                .as_deref(),
2189            Some("search"),
2190        );
2191        assert_eq!(
2192            Reflect::get(&webapp, &"types".into())
2193                .unwrap()
2194                .as_string()
2195                .as_deref(),
2196            Some("users"),
2197        );
2198    }
2199
2200    #[wasm_bindgen_test]
2201    #[allow(dead_code, clippy::unused_unit)]
2202    fn share_message_calls_js() {
2203        let webapp = setup_webapp();
2204        let share = Function::new_with_args("id, cb", "this.shared_id = id; cb(true);");
2205        let _ = Reflect::set(&webapp, &"shareMessage".into(), &share);
2206
2207        let app = TelegramWebApp::instance().unwrap();
2208        let sent = Rc::new(Cell::new(false));
2209        let sent_clone = Rc::clone(&sent);
2210
2211        app.share_message("123", move |s| {
2212            sent_clone.set(s);
2213        })
2214        .unwrap();
2215
2216        assert_eq!(
2217            Reflect::get(&webapp, &"shared_id".into())
2218                .unwrap()
2219                .as_string()
2220                .as_deref(),
2221            Some("123"),
2222        );
2223        assert!(sent.get());
2224    }
2225
2226    #[wasm_bindgen_test]
2227    #[allow(dead_code, clippy::unused_unit)]
2228    fn share_to_story_calls_js() {
2229        let webapp = setup_webapp();
2230        let share = Function::new_with_args(
2231            "url, params",
2232            "this.story_url = url; this.story_params = params;"
2233        );
2234        let _ = Reflect::set(&webapp, &"shareToStory".into(), &share);
2235
2236        let app = TelegramWebApp::instance().unwrap();
2237        let url = "https://example.com/media";
2238        let params = Object::new();
2239        let _ = Reflect::set(&params, &"text".into(), &"hi".into());
2240        app.share_to_story(url, Some(&params.into())).unwrap();
2241
2242        assert_eq!(
2243            Reflect::get(&webapp, &"story_url".into())
2244                .unwrap()
2245                .as_string()
2246                .as_deref(),
2247            Some(url),
2248        );
2249        let stored = Reflect::get(&webapp, &"story_params".into()).unwrap();
2250        assert_eq!(
2251            Reflect::get(&stored, &"text".into())
2252                .unwrap()
2253                .as_string()
2254                .as_deref(),
2255            Some("hi"),
2256        );
2257    }
2258
2259    #[wasm_bindgen_test]
2260    #[allow(dead_code, clippy::unused_unit)]
2261    fn share_url_calls_js() {
2262        let webapp = setup_webapp();
2263        let share = Function::new_with_args(
2264            "url, text",
2265            "this.shared_url = url; this.shared_text = text;"
2266        );
2267        let _ = Reflect::set(&webapp, &"shareURL".into(), &share);
2268
2269        let app = TelegramWebApp::instance().unwrap();
2270        let url = "https://example.com";
2271        let text = "check";
2272        app.share_url(url, Some(text)).unwrap();
2273
2274        assert_eq!(
2275            Reflect::get(&webapp, &"shared_url".into())
2276                .unwrap()
2277                .as_string()
2278                .as_deref(),
2279            Some(url),
2280        );
2281        assert_eq!(
2282            Reflect::get(&webapp, &"shared_text".into())
2283                .unwrap()
2284                .as_string()
2285                .as_deref(),
2286            Some(text),
2287        );
2288    }
2289
2290    #[wasm_bindgen_test]
2291    #[allow(dead_code, clippy::unused_unit)]
2292    fn join_voice_chat_calls_js() {
2293        let webapp = setup_webapp();
2294        let join = Function::new_with_args(
2295            "id, hash",
2296            "this.voice_chat_id = id; this.voice_chat_hash = hash;"
2297        );
2298        let _ = Reflect::set(&webapp, &"joinVoiceChat".into(), &join);
2299
2300        let app = TelegramWebApp::instance().unwrap();
2301        app.join_voice_chat("123", Some("hash")).unwrap();
2302
2303        assert_eq!(
2304            Reflect::get(&webapp, &"voice_chat_id".into())
2305                .unwrap()
2306                .as_string()
2307                .as_deref(),
2308            Some("123"),
2309        );
2310        assert_eq!(
2311            Reflect::get(&webapp, &"voice_chat_hash".into())
2312                .unwrap()
2313                .as_string()
2314                .as_deref(),
2315            Some("hash"),
2316        );
2317    }
2318
2319    #[wasm_bindgen_test]
2320    #[allow(dead_code, clippy::unused_unit)]
2321    fn add_to_home_screen_calls_js() {
2322        let webapp = setup_webapp();
2323        let add = Function::new_with_args("", "this.called = true; return true;");
2324        let _ = Reflect::set(&webapp, &"addToHomeScreen".into(), &add);
2325
2326        let app = TelegramWebApp::instance().unwrap();
2327        let shown = app.add_to_home_screen().unwrap();
2328        assert!(shown);
2329        let called = Reflect::get(&webapp, &"called".into())
2330            .unwrap()
2331            .as_bool()
2332            .unwrap_or(false);
2333        assert!(called);
2334    }
2335
2336    #[wasm_bindgen_test]
2337    #[allow(dead_code, clippy::unused_unit)]
2338    fn request_fullscreen_calls_js() {
2339        let webapp = setup_webapp();
2340        let called = Rc::new(Cell::new(false));
2341        let called_clone = Rc::clone(&called);
2342
2343        let cb = Closure::<dyn FnMut()>::new(move || {
2344            called_clone.set(true);
2345        });
2346        let _ = Reflect::set(
2347            &webapp,
2348            &"requestFullscreen".into(),
2349            cb.as_ref().unchecked_ref()
2350        );
2351        cb.forget();
2352
2353        let app = TelegramWebApp::instance().unwrap();
2354        app.request_fullscreen().unwrap();
2355        assert!(called.get());
2356    }
2357
2358    #[wasm_bindgen_test]
2359    #[allow(dead_code, clippy::unused_unit)]
2360    fn exit_fullscreen_calls_js() {
2361        let webapp = setup_webapp();
2362        let called = Rc::new(Cell::new(false));
2363        let called_clone = Rc::clone(&called);
2364
2365        let cb = Closure::<dyn FnMut()>::new(move || {
2366            called_clone.set(true);
2367        });
2368        let _ = Reflect::set(
2369            &webapp,
2370            &"exitFullscreen".into(),
2371            cb.as_ref().unchecked_ref()
2372        );
2373        cb.forget();
2374
2375        let app = TelegramWebApp::instance().unwrap();
2376        app.exit_fullscreen().unwrap();
2377        assert!(called.get());
2378    }
2379
2380    #[wasm_bindgen_test]
2381    #[allow(dead_code, clippy::unused_unit)]
2382    fn check_home_screen_status_invokes_callback() {
2383        let webapp = setup_webapp();
2384        let check = Function::new_with_args("cb", "cb('added');");
2385        let _ = Reflect::set(&webapp, &"checkHomeScreenStatus".into(), &check);
2386
2387        let app = TelegramWebApp::instance().unwrap();
2388        let status = Rc::new(RefCell::new(String::new()));
2389        let status_clone = Rc::clone(&status);
2390
2391        app.check_home_screen_status(move |s| {
2392            *status_clone.borrow_mut() = s;
2393        })
2394        .unwrap();
2395
2396        assert_eq!(status.borrow().as_str(), "added");
2397    }
2398
2399    #[wasm_bindgen_test]
2400    #[allow(dead_code, clippy::unused_unit)]
2401    fn lock_orientation_calls_js() {
2402        let webapp = setup_webapp();
2403        let received = Rc::new(RefCell::new(None));
2404        let rc_clone = Rc::clone(&received);
2405
2406        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2407            *rc_clone.borrow_mut() = v.as_string();
2408        });
2409        let _ = Reflect::set(
2410            &webapp,
2411            &"lockOrientation".into(),
2412            cb.as_ref().unchecked_ref()
2413        );
2414        cb.forget();
2415
2416        let app = TelegramWebApp::instance().unwrap();
2417        app.lock_orientation("portrait").unwrap();
2418        assert_eq!(received.borrow().as_deref(), Some("portrait"));
2419    }
2420
2421    #[wasm_bindgen_test]
2422    #[allow(dead_code, clippy::unused_unit)]
2423    fn unlock_orientation_calls_js() {
2424        let webapp = setup_webapp();
2425        let called = Rc::new(Cell::new(false));
2426        let called_clone = Rc::clone(&called);
2427
2428        let cb = Closure::<dyn FnMut()>::new(move || {
2429            called_clone.set(true);
2430        });
2431        let _ = Reflect::set(
2432            &webapp,
2433            &"unlockOrientation".into(),
2434            cb.as_ref().unchecked_ref()
2435        );
2436        cb.forget();
2437
2438        let app = TelegramWebApp::instance().unwrap();
2439        app.unlock_orientation().unwrap();
2440        assert!(called.get());
2441    }
2442
2443    #[wasm_bindgen_test]
2444    #[allow(dead_code, clippy::unused_unit)]
2445    fn enable_vertical_swipes_calls_js() {
2446        let webapp = setup_webapp();
2447        let called = Rc::new(Cell::new(false));
2448        let called_clone = Rc::clone(&called);
2449
2450        let cb = Closure::<dyn FnMut()>::new(move || {
2451            called_clone.set(true);
2452        });
2453        let _ = Reflect::set(
2454            &webapp,
2455            &"enableVerticalSwipes".into(),
2456            cb.as_ref().unchecked_ref()
2457        );
2458        cb.forget();
2459
2460        let app = TelegramWebApp::instance().unwrap();
2461        app.enable_vertical_swipes().unwrap();
2462        assert!(called.get());
2463    }
2464
2465    #[wasm_bindgen_test]
2466    #[allow(dead_code, clippy::unused_unit)]
2467    fn disable_vertical_swipes_calls_js() {
2468        let webapp = setup_webapp();
2469        let called = Rc::new(Cell::new(false));
2470        let called_clone = Rc::clone(&called);
2471
2472        let cb = Closure::<dyn FnMut()>::new(move || {
2473            called_clone.set(true);
2474        });
2475        let _ = Reflect::set(
2476            &webapp,
2477            &"disableVerticalSwipes".into(),
2478            cb.as_ref().unchecked_ref()
2479        );
2480        cb.forget();
2481
2482        let app = TelegramWebApp::instance().unwrap();
2483        app.disable_vertical_swipes().unwrap();
2484        assert!(called.get());
2485    }
2486
2487    #[wasm_bindgen_test]
2488    #[allow(dead_code, clippy::unused_unit)]
2489    fn request_write_access_invokes_callback() {
2490        let webapp = setup_webapp();
2491        let request = Function::new_with_args("cb", "cb(true);");
2492        let _ = Reflect::set(&webapp, &"requestWriteAccess".into(), &request);
2493
2494        let app = TelegramWebApp::instance().unwrap();
2495        let granted = Rc::new(Cell::new(false));
2496        let granted_clone = Rc::clone(&granted);
2497
2498        let res = app.request_write_access(move |g| {
2499            granted_clone.set(g);
2500        });
2501        assert!(res.is_ok());
2502
2503        assert!(granted.get());
2504    }
2505
2506    #[wasm_bindgen_test]
2507    #[allow(dead_code, clippy::unused_unit)]
2508    fn download_file_invokes_callback() {
2509        let webapp = setup_webapp();
2510        let received_url = Rc::new(RefCell::new(String::new()));
2511        let received_name = Rc::new(RefCell::new(String::new()));
2512        let url_clone = Rc::clone(&received_url);
2513        let name_clone = Rc::clone(&received_name);
2514
2515        let download = Closure::<dyn FnMut(JsValue, JsValue)>::new(move |params, cb: JsValue| {
2516            let url = Reflect::get(&params, &"url".into())
2517                .unwrap()
2518                .as_string()
2519                .unwrap_or_default();
2520            let name = Reflect::get(&params, &"file_name".into())
2521                .unwrap()
2522                .as_string()
2523                .unwrap_or_default();
2524            *url_clone.borrow_mut() = url;
2525            *name_clone.borrow_mut() = name;
2526            let func = cb.dyn_ref::<Function>().unwrap();
2527            let _ = func.call1(&JsValue::NULL, &JsValue::from_str("id"));
2528        });
2529        let _ = Reflect::set(
2530            &webapp,
2531            &"downloadFile".into(),
2532            download.as_ref().unchecked_ref()
2533        );
2534        download.forget();
2535
2536        let app = TelegramWebApp::instance().unwrap();
2537        let result = Rc::new(RefCell::new(String::new()));
2538        let result_clone = Rc::clone(&result);
2539        let params = DownloadFileParams {
2540            url:       "https://example.com/data.bin",
2541            file_name: Some("data.bin"),
2542            mime_type: None
2543        };
2544        app.download_file(params, move |id| {
2545            *result_clone.borrow_mut() = id;
2546        })
2547        .unwrap();
2548
2549        assert_eq!(
2550            received_url.borrow().as_str(),
2551            "https://example.com/data.bin"
2552        );
2553        assert_eq!(received_name.borrow().as_str(), "data.bin");
2554        assert_eq!(result.borrow().as_str(), "id");
2555    }
2556
2557    #[wasm_bindgen_test]
2558    #[allow(dead_code, clippy::unused_unit)]
2559    fn request_write_access_returns_error_when_missing() {
2560        let _webapp = setup_webapp();
2561        let app = TelegramWebApp::instance().unwrap();
2562        let res = app.request_write_access(|_| {});
2563        assert!(res.is_err());
2564    }
2565    #[wasm_bindgen_test]
2566    #[allow(dead_code, clippy::unused_unit)]
2567    fn request_emoji_status_access_invokes_callback() {
2568        let webapp = setup_webapp();
2569        let request = Function::new_with_args("cb", "cb(false);");
2570        let _ = Reflect::set(&webapp, &"requestEmojiStatusAccess".into(), &request);
2571
2572        let app = TelegramWebApp::instance().unwrap();
2573        let granted = Rc::new(Cell::new(true));
2574        let granted_clone = Rc::clone(&granted);
2575
2576        app.request_emoji_status_access(move |g| {
2577            granted_clone.set(g);
2578        })
2579        .unwrap();
2580
2581        assert!(!granted.get());
2582    }
2583
2584    #[wasm_bindgen_test]
2585    #[allow(dead_code, clippy::unused_unit)]
2586    fn set_emoji_status_invokes_callback() {
2587        let webapp = setup_webapp();
2588        let set_status = Function::new_with_args("status, cb", "this.st = status; cb(true);");
2589        let _ = Reflect::set(&webapp, &"setEmojiStatus".into(), &set_status);
2590
2591        let status = Object::new();
2592        let _ = Reflect::set(
2593            &status,
2594            &"custom_emoji_id".into(),
2595            &JsValue::from_str("321")
2596        );
2597
2598        let app = TelegramWebApp::instance().unwrap();
2599        let success = Rc::new(Cell::new(false));
2600        let success_clone = Rc::clone(&success);
2601
2602        app.set_emoji_status(&status.into(), move |s| {
2603            success_clone.set(s);
2604        })
2605        .unwrap();
2606
2607        assert!(success.get());
2608        let stored = Reflect::get(&webapp, &"st".into()).unwrap();
2609        let id = Reflect::get(&stored, &"custom_emoji_id".into())
2610            .unwrap()
2611            .as_string();
2612        assert_eq!(id.as_deref(), Some("321"));
2613    }
2614
2615    #[wasm_bindgen_test]
2616    #[allow(dead_code, clippy::unused_unit)]
2617    fn show_popup_invokes_callback() {
2618        let webapp = setup_webapp();
2619        let show_popup = Function::new_with_args("params, cb", "cb('ok');");
2620        let _ = Reflect::set(&webapp, &"showPopup".into(), &show_popup);
2621
2622        let app = TelegramWebApp::instance().unwrap();
2623        let button = Rc::new(RefCell::new(String::new()));
2624        let button_clone = Rc::clone(&button);
2625
2626        app.show_popup(&JsValue::NULL, move |id| {
2627            *button_clone.borrow_mut() = id;
2628        })
2629        .unwrap();
2630
2631        assert_eq!(button.borrow().as_str(), "ok");
2632    }
2633
2634    #[wasm_bindgen_test]
2635    #[allow(dead_code, clippy::unused_unit)]
2636    fn read_text_from_clipboard_invokes_callback() {
2637        let webapp = setup_webapp();
2638        let read_clip = Function::new_with_args("cb", "cb('clip');");
2639        let _ = Reflect::set(&webapp, &"readTextFromClipboard".into(), &read_clip);
2640
2641        let app = TelegramWebApp::instance().unwrap();
2642        let text = Rc::new(RefCell::new(String::new()));
2643        let text_clone = Rc::clone(&text);
2644
2645        app.read_text_from_clipboard(move |t| {
2646            *text_clone.borrow_mut() = t;
2647        })
2648        .unwrap();
2649
2650        assert_eq!(text.borrow().as_str(), "clip");
2651    }
2652
2653    #[wasm_bindgen_test]
2654    #[allow(dead_code, clippy::unused_unit)]
2655    fn scan_qr_popup_invokes_callback_and_close() {
2656        let webapp = setup_webapp();
2657        let show_scan = Function::new_with_args("text, cb", "cb('code');");
2658        let close_scan = Function::new_with_args("", "this.closed = true;");
2659        let _ = Reflect::set(&webapp, &"showScanQrPopup".into(), &show_scan);
2660        let _ = Reflect::set(&webapp, &"closeScanQrPopup".into(), &close_scan);
2661
2662        let app = TelegramWebApp::instance().unwrap();
2663        let text = Rc::new(RefCell::new(String::new()));
2664        let text_clone = Rc::clone(&text);
2665
2666        app.show_scan_qr_popup("scan", move |value| {
2667            *text_clone.borrow_mut() = value;
2668        })
2669        .unwrap();
2670        assert_eq!(text.borrow().as_str(), "code");
2671
2672        app.close_scan_qr_popup().unwrap();
2673        let closed = Reflect::get(&webapp, &"closed".into())
2674            .unwrap()
2675            .as_bool()
2676            .unwrap_or(false);
2677        assert!(closed);
2678    }
2679}