telegram_webapp_sdk/
webapp.rs

1use js_sys::{Function, Object, Reflect};
2use serde::Serialize;
3use serde_wasm_bindgen::to_value;
4use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
5use web_sys::window;
6
7use crate::{
8    core::types::download_file_params::DownloadFileParams,
9    logger,
10    validate_init_data::{self, ValidationKey}
11};
12
13/// Handle returned when registering callbacks.
14pub struct EventHandle<T: ?Sized> {
15    target:   Object,
16    method:   &'static str,
17    event:    Option<String>,
18    callback: Closure<T>
19}
20
21impl<T: ?Sized> EventHandle<T> {
22    fn new(
23        target: Object,
24        method: &'static str,
25        event: Option<String>,
26        callback: Closure<T>
27    ) -> Self {
28        Self {
29            target,
30            method,
31            event,
32            callback
33        }
34    }
35
36    pub(crate) fn unregister(self) -> Result<(), JsValue> {
37        let f = Reflect::get(&self.target, &self.method.into())?;
38        let func = f
39            .dyn_ref::<Function>()
40            .ok_or_else(|| JsValue::from_str(&format!("{} is not a function", self.method)))?;
41        match self.event {
42            Some(event) => func.call2(
43                &self.target,
44                &event.into(),
45                self.callback.as_ref().unchecked_ref()
46            )?,
47            None => func.call1(&self.target, self.callback.as_ref().unchecked_ref())?
48        };
49        Ok(())
50    }
51}
52
53/// Identifies which bottom button to operate on.
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
55pub enum BottomButton {
56    /// Primary bottom button.
57    Main,
58    /// Secondary bottom button.
59    Secondary
60}
61
62impl BottomButton {
63    const fn js_name(self) -> &'static str {
64        match self {
65            BottomButton::Main => "MainButton",
66            BottomButton::Secondary => "SecondaryButton"
67        }
68    }
69}
70
71/// Position of the secondary bottom button.
72///
73/// # Examples
74/// ```no_run
75/// use telegram_webapp_sdk::webapp::{SecondaryButtonPosition, TelegramWebApp};
76///
77/// if let Some(app) = TelegramWebApp::instance() {
78///     match app.secondary_button_position() {
79///         Some(SecondaryButtonPosition::Top) => {}
80///         _ => {}
81///     }
82/// }
83/// ```
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
85#[serde(rename_all = "lowercase")]
86pub enum SecondaryButtonPosition {
87    /// Displayed above the main button.
88    Top,
89    /// Displayed to the left of the main button.
90    Left,
91    /// Displayed below the main button.
92    Bottom,
93    /// Displayed to the right of the main button.
94    Right
95}
96
97impl SecondaryButtonPosition {
98    fn from_js_value(value: JsValue) -> Option<Self> {
99        let as_str = value.as_string()?;
100        match as_str.as_str() {
101            "top" => Some(Self::Top),
102            "left" => Some(Self::Left),
103            "bottom" => Some(Self::Bottom),
104            "right" => Some(Self::Right),
105            _ => None
106        }
107    }
108}
109
110/// Safe area insets reported by Telegram.
111///
112/// # Examples
113/// ```no_run
114/// use telegram_webapp_sdk::webapp::{SafeAreaInset, TelegramWebApp};
115///
116/// if let Some(app) = TelegramWebApp::instance() {
117///     if let Some(SafeAreaInset {
118///         top,
119///         bottom,
120///         ..
121///     }) = app.safe_area_inset()
122///     {
123///         let _ = (top, bottom);
124///     }
125/// }
126/// ```
127#[derive(Clone, Copy, Debug, PartialEq)]
128pub struct SafeAreaInset {
129    /// Distance from the top edge in CSS pixels.
130    pub top:    f64,
131    /// Distance from the bottom edge in CSS pixels.
132    pub bottom: f64,
133    /// Distance from the left edge in CSS pixels.
134    pub left:   f64,
135    /// Distance from the right edge in CSS pixels.
136    pub right:  f64
137}
138
139impl SafeAreaInset {
140    fn from_js(value: JsValue) -> Option<Self> {
141        let object = value.dyn_into::<Object>().ok()?;
142        let top = Reflect::get(&object, &"top".into()).ok()?.as_f64()?;
143        let bottom = Reflect::get(&object, &"bottom".into()).ok()?.as_f64()?;
144        let left = Reflect::get(&object, &"left".into()).ok()?.as_f64()?;
145        let right = Reflect::get(&object, &"right".into()).ok()?.as_f64()?;
146        Some(Self {
147            top,
148            bottom,
149            left,
150            right
151        })
152    }
153}
154
155/// Parameters accepted by bottom buttons when updating state via `setParams`.
156///
157/// # Examples
158/// ```no_run
159/// use telegram_webapp_sdk::webapp::{BottomButton, BottomButtonParams, TelegramWebApp};
160///
161/// if let Some(app) = TelegramWebApp::instance() {
162///     let params = BottomButtonParams {
163///         text: Some("Send"),
164///         is_active: Some(true),
165///         ..Default::default()
166///     };
167///     let _ = app.set_bottom_button_params(BottomButton::Main, &params);
168/// }
169/// ```
170#[derive(Debug, Default, Serialize)]
171pub struct BottomButtonParams<'a> {
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub text:             Option<&'a str>,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub color:            Option<&'a str>,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub text_color:       Option<&'a str>,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub is_active:        Option<bool>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub is_visible:       Option<bool>,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub has_shine_effect: Option<bool>
184}
185
186/// Additional parameters supported by the secondary button.
187///
188/// # Examples
189/// ```no_run
190/// use telegram_webapp_sdk::webapp::{
191///     SecondaryButtonParams, SecondaryButtonPosition, TelegramWebApp
192/// };
193///
194/// if let Some(app) = TelegramWebApp::instance() {
195///     let params = SecondaryButtonParams {
196///         common:   Default::default(),
197///         position: Some(SecondaryButtonPosition::Top)
198///     };
199///     let _ = app.set_secondary_button_params(&params);
200/// }
201/// ```
202#[derive(Debug, Default, Serialize)]
203pub struct SecondaryButtonParams<'a> {
204    #[serde(flatten)]
205    pub common:   BottomButtonParams<'a>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub position: Option<SecondaryButtonPosition>
208}
209
210/// Options supported by [`TelegramWebApp::open_link`].
211///
212/// # Examples
213/// ```no_run
214/// use telegram_webapp_sdk::webapp::{OpenLinkOptions, TelegramWebApp};
215///
216/// if let Some(app) = TelegramWebApp::instance() {
217///     let options = OpenLinkOptions {
218///         try_instant_view: Some(true)
219///     };
220///     let _ = app.open_link("https://example.com", Some(&options));
221/// }
222/// ```
223#[derive(Debug, Default, Serialize)]
224pub struct OpenLinkOptions {
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub try_instant_view: Option<bool>
227}
228
229/// Background events delivered by Telegram when the Mini App runs in the
230/// background.
231#[derive(Clone, Copy, Debug)]
232pub enum BackgroundEvent {
233    /// The main button was clicked. Payload: [`JsValue::UNDEFINED`].
234    MainButtonClicked,
235    /// The back button was clicked. Payload: [`JsValue::UNDEFINED`].
236    BackButtonClicked,
237    /// The settings button was clicked. Payload: [`JsValue::UNDEFINED`].
238    SettingsButtonClicked,
239    /// User responded to a write access request. Payload: `bool`.
240    WriteAccessRequested,
241    /// User responded to a contact request. Payload: `bool`.
242    ContactRequested,
243    /// User responded to a phone number request. Payload: `bool`.
244    PhoneRequested,
245    /// An invoice was closed. Payload: status string.
246    InvoiceClosed,
247    /// A popup was closed. Payload: object containing `button_id`.
248    PopupClosed,
249    /// Text was received from the QR scanner. Payload: scanned text.
250    QrTextReceived,
251    /// Text was read from the clipboard. Payload: clipboard text.
252    ClipboardTextReceived
253}
254
255impl BackgroundEvent {
256    const fn as_str(self) -> &'static str {
257        match self {
258            BackgroundEvent::MainButtonClicked => "mainButtonClicked",
259            BackgroundEvent::BackButtonClicked => "backButtonClicked",
260            BackgroundEvent::SettingsButtonClicked => "settingsButtonClicked",
261            BackgroundEvent::WriteAccessRequested => "writeAccessRequested",
262            BackgroundEvent::ContactRequested => "contactRequested",
263            BackgroundEvent::PhoneRequested => "phoneRequested",
264            BackgroundEvent::InvoiceClosed => "invoiceClosed",
265            BackgroundEvent::PopupClosed => "popupClosed",
266            BackgroundEvent::QrTextReceived => "qrTextReceived",
267            BackgroundEvent::ClipboardTextReceived => "clipboardTextReceived"
268        }
269    }
270}
271
272/// Safe wrapper around `window.Telegram.WebApp`
273#[derive(Clone)]
274pub struct TelegramWebApp {
275    inner: Object
276}
277
278impl TelegramWebApp {
279    /// Get instance of `Telegram.WebApp` or `None` if not present
280    pub fn instance() -> Option<Self> {
281        let win = window()?;
282        let tg = Reflect::get(&win, &"Telegram".into()).ok()?;
283        let webapp = Reflect::get(&tg, &"WebApp".into()).ok()?;
284        webapp.dyn_into::<Object>().ok().map(|inner| Self {
285            inner
286        })
287    }
288
289    /// Try to get instance of `Telegram.WebApp`.
290    ///
291    /// # Errors
292    /// Returns [`JsValue`] if the `Telegram.WebApp` object is missing or
293    /// malformed.
294    pub fn try_instance() -> Result<Self, JsValue> {
295        let win = window().ok_or_else(|| JsValue::from_str("window not available"))?;
296        let tg = Reflect::get(&win, &"Telegram".into())?;
297        let webapp = Reflect::get(&tg, &"WebApp".into())?;
298        let inner = webapp.dyn_into::<Object>()?;
299        Ok(Self {
300            inner
301        })
302    }
303
304    /// Validate an `initData` payload using either HMAC-SHA256 or Ed25519.
305    ///
306    /// Pass [`ValidationKey::BotToken`] to verify the `hash` parameter using
307    /// the bot token. Use [`ValidationKey::Ed25519PublicKey`] to verify the
308    /// `signature` parameter with an Ed25519 public key.
309    ///
310    /// # Errors
311    /// Returns [`validate_init_data::ValidationError`] if validation fails.
312    ///
313    /// # Examples
314    /// ```no_run
315    /// use telegram_webapp_sdk::{TelegramWebApp, validate_init_data::ValidationKey};
316    /// let bot_token = "123456:ABC";
317    /// let query = "a=1&b=2&hash=9e5e8d7c0b1f9f3a";
318    /// TelegramWebApp::validate_init_data(query, ValidationKey::BotToken(bot_token)).unwrap();
319    /// ```
320    pub fn validate_init_data(
321        init_data: &str,
322        key: ValidationKey
323    ) -> Result<(), validate_init_data::ValidationError> {
324        match key {
325            ValidationKey::BotToken(token) => {
326                validate_init_data::verify_hmac_sha256(init_data, token)
327            }
328            ValidationKey::Ed25519PublicKey(pk) => {
329                validate_init_data::verify_ed25519(init_data, pk)
330            }
331        }
332    }
333
334    /// Call `WebApp.sendData(data)`.
335    ///
336    /// # Errors
337    /// Returns [`JsValue`] if the underlying JS call fails.
338    pub fn send_data(&self, data: &str) -> Result<(), JsValue> {
339        self.call1("sendData", &data.into())
340    }
341
342    /// Call `WebApp.expand()`.
343    ///
344    /// # Errors
345    /// Returns [`JsValue`] if the underlying JS call fails.
346    pub fn expand(&self) -> Result<(), JsValue> {
347        self.call0("expand")
348    }
349
350    /// Call `WebApp.close()`.
351    ///
352    /// # Errors
353    /// Returns [`JsValue`] if the underlying JS call fails.
354    pub fn close(&self) -> Result<(), JsValue> {
355        self.call0("close")
356    }
357
358    /// Call `WebApp.enableClosingConfirmation()`.
359    ///
360    /// # Examples
361    /// ```no_run
362    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
363    /// # let app = TelegramWebApp::instance().unwrap();
364    /// app.enable_closing_confirmation().unwrap();
365    /// ```
366    ///
367    /// # Errors
368    /// Returns [`JsValue`] if the underlying JS call fails.
369    pub fn enable_closing_confirmation(&self) -> Result<(), JsValue> {
370        self.call0("enableClosingConfirmation")
371    }
372
373    /// Call `WebApp.disableClosingConfirmation()`.
374    ///
375    /// # Examples
376    /// ```no_run
377    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
378    /// # let app = TelegramWebApp::instance().unwrap();
379    /// app.disable_closing_confirmation().unwrap();
380    /// ```
381    ///
382    /// # Errors
383    /// Returns [`JsValue`] if the underlying JS call fails.
384    pub fn disable_closing_confirmation(&self) -> Result<(), JsValue> {
385        self.call0("disableClosingConfirmation")
386    }
387
388    /// Returns whether closing confirmation is currently enabled.
389    ///
390    /// # Examples
391    /// ```no_run
392    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
393    /// # let app = TelegramWebApp::instance().unwrap();
394    /// let _ = app.is_closing_confirmation_enabled();
395    /// ```
396    pub fn is_closing_confirmation_enabled(&self) -> bool {
397        Reflect::get(&self.inner, &"isClosingConfirmationEnabled".into())
398            .ok()
399            .and_then(|v| v.as_bool())
400            .unwrap_or(false)
401    }
402
403    /// Call `WebApp.requestFullscreen()`.
404    ///
405    /// # Examples
406    /// ```no_run
407    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
408    /// # let app = TelegramWebApp::instance().unwrap();
409    /// app.request_fullscreen().unwrap();
410    /// ```
411    ///
412    /// # Errors
413    /// Returns [`JsValue`] if the underlying JS call fails.
414    pub fn request_fullscreen(&self) -> Result<(), JsValue> {
415        self.call0("requestFullscreen")
416    }
417
418    /// Call `WebApp.exitFullscreen()`.
419    ///
420    /// # Examples
421    /// ```no_run
422    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
423    /// # let app = TelegramWebApp::instance().unwrap();
424    /// app.exit_fullscreen().unwrap();
425    /// ```
426    ///
427    /// # Errors
428    /// Returns [`JsValue`] if the underlying JS call fails.
429    pub fn exit_fullscreen(&self) -> Result<(), JsValue> {
430        self.call0("exitFullscreen")
431    }
432
433    /// Call `WebApp.lockOrientation(orientation)`.
434    ///
435    /// # Examples
436    /// ```no_run
437    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
438    /// # let app = TelegramWebApp::instance().unwrap();
439    /// app.lock_orientation("portrait").unwrap();
440    /// ```
441    ///
442    /// # Errors
443    /// Returns [`JsValue`] if the underlying JS call fails.
444    pub fn lock_orientation(&self, orientation: &str) -> Result<(), JsValue> {
445        self.call1("lockOrientation", &orientation.into())
446    }
447
448    /// Call `WebApp.unlockOrientation()`.
449    ///
450    /// # Examples
451    /// ```no_run
452    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
453    /// # let app = TelegramWebApp::instance().unwrap();
454    /// app.unlock_orientation().unwrap();
455    /// ```
456    ///
457    /// # Errors
458    /// Returns [`JsValue`] if the underlying JS call fails.
459    pub fn unlock_orientation(&self) -> Result<(), JsValue> {
460        self.call0("unlockOrientation")
461    }
462
463    /// Call `WebApp.enableVerticalSwipes()`.
464    ///
465    /// # Examples
466    /// ```no_run
467    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
468    /// # let app = TelegramWebApp::instance().unwrap();
469    /// app.enable_vertical_swipes().unwrap();
470    /// ```
471    ///
472    /// # Errors
473    /// Returns [`JsValue`] if the underlying JS call fails.
474    pub fn enable_vertical_swipes(&self) -> Result<(), JsValue> {
475        self.call0("enableVerticalSwipes")
476    }
477
478    /// Call `WebApp.disableVerticalSwipes()`.
479    ///
480    /// # Examples
481    /// ```no_run
482    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
483    /// # let app = TelegramWebApp::instance().unwrap();
484    /// app.disable_vertical_swipes().unwrap();
485    /// ```
486    ///
487    /// # Errors
488    /// Returns [`JsValue`] if the underlying JS call fails.
489    pub fn disable_vertical_swipes(&self) -> Result<(), JsValue> {
490        self.call0("disableVerticalSwipes")
491    }
492
493    /// Call `WebApp.showAlert(message)`.
494    ///
495    /// # Errors
496    /// Returns [`JsValue`] if the underlying JS call fails.
497    pub fn show_alert(&self, msg: &str) -> Result<(), JsValue> {
498        self.call1("showAlert", &msg.into())
499    }
500
501    /// Call `WebApp.showConfirm(message, callback)`.
502    ///
503    /// # Errors
504    /// Returns [`JsValue`] if the underlying JS call fails.
505    pub fn show_confirm<F>(&self, msg: &str, on_confirm: F) -> Result<(), JsValue>
506    where
507        F: 'static + Fn(bool)
508    {
509        let cb = Closure::<dyn FnMut(bool)>::new(on_confirm);
510        let f = Reflect::get(&self.inner, &"showConfirm".into())?;
511        let func = f
512            .dyn_ref::<Function>()
513            .ok_or_else(|| JsValue::from_str("showConfirm is not a function"))?;
514        func.call2(&self.inner, &msg.into(), cb.as_ref().unchecked_ref())?;
515        cb.forget(); // safe leak for JS lifetime
516        Ok(())
517    }
518
519    /// Call `WebApp.openLink(url)`.
520    ///
521    /// # Examples
522    /// ```no_run
523    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
524    /// # let app = TelegramWebApp::instance().unwrap();
525    /// app.open_link("https://example.com", None).unwrap();
526    /// ```
527    pub fn open_link(&self, url: &str, options: Option<&OpenLinkOptions>) -> Result<(), JsValue> {
528        let f = Reflect::get(&self.inner, &"openLink".into())?;
529        let func = f
530            .dyn_ref::<Function>()
531            .ok_or_else(|| JsValue::from_str("openLink is not a function"))?;
532        match options {
533            Some(opts) => {
534                let value = to_value(opts).map_err(|err| JsValue::from_str(&err.to_string()))?;
535                func.call2(&self.inner, &url.into(), &value)?;
536            }
537            None => {
538                func.call1(&self.inner, &url.into())?;
539            }
540        }
541        Ok(())
542    }
543
544    /// Call `WebApp.openTelegramLink(url)`.
545    ///
546    /// # Examples
547    /// ```no_run
548    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
549    /// # let app = TelegramWebApp::instance().unwrap();
550    /// app.open_telegram_link("https://t.me/telegram").unwrap();
551    /// ```
552    pub fn open_telegram_link(&self, url: &str) -> Result<(), JsValue> {
553        Reflect::get(&self.inner, &"openTelegramLink".into())?
554            .dyn_into::<Function>()?
555            .call1(&self.inner, &url.into())?;
556        Ok(())
557    }
558
559    /// Returns whether the WebApp version is at least the provided value.
560    ///
561    /// # Examples
562    /// ```no_run
563    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
564    ///
565    /// if let Some(app) = TelegramWebApp::instance() {
566    ///     let _ = app.is_version_at_least("9.0");
567    /// }
568    /// ```
569    pub fn is_version_at_least(&self, version: &str) -> Result<bool, JsValue> {
570        let f = Reflect::get(&self.inner, &"isVersionAtLeast".into())?;
571        let func = f
572            .dyn_ref::<Function>()
573            .ok_or_else(|| JsValue::from_str("isVersionAtLeast is not a function"))?;
574        let result = func.call1(&self.inner, &version.into())?;
575        Ok(result.as_bool().unwrap_or(false))
576    }
577
578    /// Call `WebApp.openInvoice(url, callback)`.
579    ///
580    /// # Examples
581    /// ```no_run
582    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
583    /// # let app = TelegramWebApp::instance().unwrap();
584    /// app.open_invoice("https://invoice", |status| {
585    ///     let _ = status;
586    /// })
587    /// .unwrap();
588    /// ```
589    pub fn open_invoice<F>(&self, url: &str, callback: F) -> Result<(), JsValue>
590    where
591        F: 'static + Fn(String)
592    {
593        let cb = Closure::<dyn FnMut(JsValue)>::new(move |status: JsValue| {
594            callback(status.as_string().unwrap_or_default());
595        });
596        Reflect::get(&self.inner, &"openInvoice".into())?
597            .dyn_into::<Function>()?
598            .call2(&self.inner, &url.into(), cb.as_ref().unchecked_ref())?;
599        cb.forget();
600        Ok(())
601    }
602
603    /// Call `WebApp.switchInlineQuery(query, choose_chat_types)`.
604    ///
605    /// # Examples
606    /// ```no_run
607    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
608    /// # let app = TelegramWebApp::instance().unwrap();
609    /// app.switch_inline_query("query", None).unwrap();
610    /// ```
611    ///
612    /// # Errors
613    /// Returns [`JsValue`] if the underlying JS call fails.
614    pub fn switch_inline_query(
615        &self,
616        query: &str,
617        choose_chat_types: Option<&JsValue>
618    ) -> Result<(), JsValue> {
619        let f = Reflect::get(&self.inner, &"switchInlineQuery".into())?;
620        let func = f
621            .dyn_ref::<Function>()
622            .ok_or_else(|| JsValue::from_str("switchInlineQuery is not a function"))?;
623        match choose_chat_types {
624            Some(types) => func.call2(&self.inner, &query.into(), types)?,
625            None => func.call1(&self.inner, &query.into())?
626        };
627        Ok(())
628    }
629
630    /// Call `WebApp.shareMessage(msg_id, callback)`.
631    ///
632    /// # Examples
633    /// ```no_run
634    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
635    /// # let app = TelegramWebApp::instance().unwrap();
636    /// app.share_message("id123", |sent| {
637    ///     let _ = sent;
638    /// })
639    /// .unwrap();
640    /// ```
641    ///
642    /// # Errors
643    /// Returns [`JsValue`] if the underlying JS call fails.
644    pub fn share_message<F>(&self, msg_id: &str, callback: F) -> Result<(), JsValue>
645    where
646        F: 'static + Fn(bool)
647    {
648        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
649            callback(v.as_bool().unwrap_or(false));
650        });
651        let f = Reflect::get(&self.inner, &"shareMessage".into())?;
652        let func = f
653            .dyn_ref::<Function>()
654            .ok_or_else(|| JsValue::from_str("shareMessage is not a function"))?;
655        func.call2(&self.inner, &msg_id.into(), cb.as_ref().unchecked_ref())?;
656        cb.forget();
657        Ok(())
658    }
659
660    /// Call `WebApp.shareToStory(media_url, params)`.
661    ///
662    /// # Examples
663    /// ```no_run
664    /// # use js_sys::Object;
665    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
666    /// # let app = TelegramWebApp::instance().unwrap();
667    /// let params = Object::new();
668    /// app.share_to_story("https://example.com/image.png", Some(&params.into()))
669    ///     .unwrap();
670    /// ```
671    ///
672    /// # Errors
673    /// Returns [`JsValue`] if the underlying JS call fails.
674    pub fn share_to_story(
675        &self,
676        media_url: &str,
677        params: Option<&JsValue>
678    ) -> Result<(), JsValue> {
679        let f = Reflect::get(&self.inner, &"shareToStory".into())?;
680        let func = f
681            .dyn_ref::<Function>()
682            .ok_or_else(|| JsValue::from_str("shareToStory is not a function"))?;
683        match params {
684            Some(p) => func.call2(&self.inner, &media_url.into(), p)?,
685            None => func.call1(&self.inner, &media_url.into())?
686        };
687        Ok(())
688    }
689
690    /// Call `WebApp.shareURL(url, text)`.
691    ///
692    /// # Examples
693    /// ```no_run
694    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
695    /// # let app = TelegramWebApp::instance().unwrap();
696    /// app.share_url("https://example.com", Some("Check this"))
697    ///     .unwrap();
698    /// ```
699    ///
700    /// # Errors
701    /// Returns [`JsValue`] if the underlying JS call fails.
702    pub fn share_url(&self, url: &str, text: Option<&str>) -> Result<(), JsValue> {
703        let f = Reflect::get(&self.inner, &"shareURL".into())?;
704        let func = f
705            .dyn_ref::<Function>()
706            .ok_or_else(|| JsValue::from_str("shareURL is not a function"))?;
707        match text {
708            Some(t) => func.call2(&self.inner, &url.into(), &t.into())?,
709            None => func.call1(&self.inner, &url.into())?
710        };
711        Ok(())
712    }
713
714    /// Call `WebApp.joinVoiceChat(chat_id, invite_hash)`.
715    ///
716    /// # Examples
717    /// ```no_run
718    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
719    /// # let app = TelegramWebApp::instance().unwrap();
720    /// app.join_voice_chat("chat", None).unwrap();
721    /// ```
722    ///
723    /// # Errors
724    /// Returns [`JsValue`] if the underlying JS call fails.
725    pub fn join_voice_chat(
726        &self,
727        chat_id: &str,
728        invite_hash: Option<&str>
729    ) -> Result<(), JsValue> {
730        let f = Reflect::get(&self.inner, &"joinVoiceChat".into())?;
731        let func = f
732            .dyn_ref::<Function>()
733            .ok_or_else(|| JsValue::from_str("joinVoiceChat is not a function"))?;
734        match invite_hash {
735            Some(hash) => func.call2(&self.inner, &chat_id.into(), &hash.into())?,
736            None => func.call1(&self.inner, &chat_id.into())?
737        };
738        Ok(())
739    }
740
741    /// Call `WebApp.addToHomeScreen()` and return whether the prompt was shown.
742    ///
743    /// # Examples
744    /// ```no_run
745    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
746    /// # let app = TelegramWebApp::instance().unwrap();
747    /// let _shown = app.add_to_home_screen().unwrap();
748    /// ```
749    pub fn add_to_home_screen(&self) -> Result<bool, JsValue> {
750        let f = Reflect::get(&self.inner, &"addToHomeScreen".into())?;
751        let func = f
752            .dyn_ref::<Function>()
753            .ok_or_else(|| JsValue::from_str("addToHomeScreen is not a function"))?;
754        let result = func.call0(&self.inner)?;
755        Ok(result.as_bool().unwrap_or(false))
756    }
757
758    /// Call `WebApp.checkHomeScreenStatus(callback)`.
759    ///
760    /// # Examples
761    /// ```no_run
762    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
763    /// # let app = TelegramWebApp::instance().unwrap();
764    /// app.check_home_screen_status(|status| {
765    ///     let _ = status;
766    /// })
767    /// .unwrap();
768    /// ```
769    pub fn check_home_screen_status<F>(&self, callback: F) -> Result<(), JsValue>
770    where
771        F: 'static + Fn(String)
772    {
773        let cb = Closure::<dyn FnMut(JsValue)>::new(move |status: JsValue| {
774            callback(status.as_string().unwrap_or_default());
775        });
776        let f = Reflect::get(&self.inner, &"checkHomeScreenStatus".into())?;
777        let func = f
778            .dyn_ref::<Function>()
779            .ok_or_else(|| JsValue::from_str("checkHomeScreenStatus is not a function"))?;
780        func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
781        cb.forget();
782        Ok(())
783    }
784
785    /// Call `WebApp.requestWriteAccess(callback)`.
786    ///
787    /// # Examples
788    /// ```no_run
789    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
790    /// # let app = TelegramWebApp::instance().unwrap();
791    /// app.request_write_access(|granted| {
792    ///     let _ = granted;
793    /// })
794    /// .unwrap();
795    /// ```
796    ///
797    /// # Errors
798    /// Returns [`JsValue`] if the underlying JS call fails.
799    pub fn request_write_access<F>(&self, callback: F) -> Result<(), JsValue>
800    where
801        F: 'static + Fn(bool)
802    {
803        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
804            callback(v.as_bool().unwrap_or(false));
805        });
806        self.call1("requestWriteAccess", cb.as_ref().unchecked_ref())?;
807        cb.forget();
808        Ok(())
809    }
810
811    /// Call `WebApp.downloadFile(params, callback)`.
812    ///
813    /// # Examples
814    /// ```no_run
815    /// # use telegram_webapp_sdk::core::types::download_file_params::DownloadFileParams;
816    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
817    /// # let app = TelegramWebApp::instance().unwrap();
818    /// let params = DownloadFileParams {
819    ///     url:       "https://example.com/file",
820    ///     file_name: None,
821    ///     mime_type: None
822    /// };
823    /// app.download_file(params, |file_id| {
824    ///     let _ = file_id;
825    /// })
826    /// .unwrap();
827    /// ```
828    ///
829    /// # Errors
830    /// Returns [`JsValue`] if the underlying JS call fails or the parameters
831    /// fail to serialize.
832    pub fn download_file<F>(
833        &self,
834        params: DownloadFileParams<'_>,
835        callback: F
836    ) -> Result<(), JsValue>
837    where
838        F: 'static + Fn(String)
839    {
840        let js_params =
841            to_value(&params).map_err(|e| JsValue::from_str(&format!("serialize params: {e}")))?;
842        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
843            callback(v.as_string().unwrap_or_default());
844        });
845        Reflect::get(&self.inner, &"downloadFile".into())?
846            .dyn_into::<Function>()?
847            .call2(&self.inner, &js_params, cb.as_ref().unchecked_ref())?;
848        cb.forget();
849        Ok(())
850    }
851
852    /// Call `WebApp.requestEmojiStatusAccess(callback)`.
853    ///
854    /// # Examples
855    /// ```no_run
856    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
857    /// # let app = TelegramWebApp::instance().unwrap();
858    /// app.request_emoji_status_access(|granted| {
859    ///     let _ = granted;
860    /// })
861    /// .unwrap();
862    /// ```
863    ///
864    /// # Errors
865    /// Returns [`JsValue`] if the underlying JS call fails.
866    pub fn request_emoji_status_access<F>(&self, callback: F) -> Result<(), JsValue>
867    where
868        F: 'static + Fn(bool)
869    {
870        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
871            callback(v.as_bool().unwrap_or(false));
872        });
873        let f = Reflect::get(&self.inner, &"requestEmojiStatusAccess".into())?;
874        let func = f
875            .dyn_ref::<Function>()
876            .ok_or_else(|| JsValue::from_str("requestEmojiStatusAccess is not a function"))?;
877        func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
878        cb.forget();
879        Ok(())
880    }
881
882    /// Call `WebApp.setEmojiStatus(status, callback)`.
883    ///
884    /// # Examples
885    /// ```no_run
886    /// # use js_sys::Object;
887    /// # use js_sys::Reflect;
888    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
889    /// # let app = TelegramWebApp::instance().unwrap();
890    /// let status = Object::new();
891    /// let _ = Reflect::set(&status, &"custom_emoji_id".into(), &"123".into());
892    /// app.set_emoji_status(&status.into(), |success| {
893    ///     let _ = success;
894    /// })
895    /// .unwrap();
896    /// ```
897    ///
898    /// # Errors
899    /// Returns [`JsValue`] if the underlying JS call fails.
900    pub fn set_emoji_status<F>(&self, status: &JsValue, callback: F) -> Result<(), JsValue>
901    where
902        F: 'static + Fn(bool)
903    {
904        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
905            callback(v.as_bool().unwrap_or(false));
906        });
907        let f = Reflect::get(&self.inner, &"setEmojiStatus".into())?;
908        let func = f
909            .dyn_ref::<Function>()
910            .ok_or_else(|| JsValue::from_str("setEmojiStatus is not a function"))?;
911        func.call2(&self.inner, status, cb.as_ref().unchecked_ref())?;
912        cb.forget();
913        Ok(())
914    }
915
916    /// Call `WebApp.showPopup(params, callback)`.
917    ///
918    /// # Examples
919    /// ```no_run
920    /// # use js_sys::Object;
921    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
922    /// # let app = TelegramWebApp::instance().unwrap();
923    /// let params = Object::new();
924    /// app.show_popup(&params.into(), |id| {
925    ///     let _ = id;
926    /// })
927    /// .unwrap();
928    /// ```
929    pub fn show_popup<F>(&self, params: &JsValue, callback: F) -> Result<(), JsValue>
930    where
931        F: 'static + Fn(String)
932    {
933        let cb = Closure::<dyn FnMut(JsValue)>::new(move |id: JsValue| {
934            callback(id.as_string().unwrap_or_default());
935        });
936        Reflect::get(&self.inner, &"showPopup".into())?
937            .dyn_into::<Function>()?
938            .call2(&self.inner, params, cb.as_ref().unchecked_ref())?;
939        cb.forget();
940        Ok(())
941    }
942
943    /// Call `WebApp.showScanQrPopup(text, callback)`.
944    ///
945    /// # Examples
946    /// ```no_run
947    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
948    /// # let app = TelegramWebApp::instance().unwrap();
949    /// app.show_scan_qr_popup("Scan", |text| {
950    ///     let _ = text;
951    /// })
952    /// .unwrap();
953    /// ```
954    pub fn show_scan_qr_popup<F>(&self, text: &str, callback: F) -> Result<(), JsValue>
955    where
956        F: 'static + Fn(String)
957    {
958        let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
959            callback(value.as_string().unwrap_or_default());
960        });
961        Reflect::get(&self.inner, &"showScanQrPopup".into())?
962            .dyn_into::<Function>()?
963            .call2(&self.inner, &text.into(), cb.as_ref().unchecked_ref())?;
964        cb.forget();
965        Ok(())
966    }
967
968    /// Call `WebApp.closeScanQrPopup()`.
969    ///
970    /// # Examples
971    /// ```no_run
972    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
973    /// # let app = TelegramWebApp::instance().unwrap();
974    /// app.close_scan_qr_popup().unwrap();
975    /// ```
976    pub fn close_scan_qr_popup(&self) -> Result<(), JsValue> {
977        Reflect::get(&self.inner, &"closeScanQrPopup".into())?
978            .dyn_into::<Function>()?
979            .call0(&self.inner)?;
980        Ok(())
981    }
982
983    /// Call `WebApp.hideKeyboard()`.
984    fn bottom_button_object(&self, button: BottomButton) -> Result<Object, JsValue> {
985        let name = button.js_name();
986        Reflect::get(&self.inner, &name.into())
987            .inspect_err(|_| logger::error(&format!("{name} not available")))?
988            .dyn_into::<Object>()
989            .inspect_err(|_| logger::error(&format!("{name} is not an object")))
990    }
991
992    fn bottom_button_method(
993        &self,
994        button: BottomButton,
995        method: &str,
996        arg: Option<&JsValue>
997    ) -> Result<(), JsValue> {
998        let name = button.js_name();
999        let btn = self.bottom_button_object(button)?;
1000        let f = Reflect::get(&btn, &method.into())
1001            .inspect_err(|_| logger::error(&format!("{name}.{method} not available")))?;
1002        let func = f.dyn_ref::<Function>().ok_or_else(|| {
1003            logger::error(&format!("{name}.{method} is not a function"));
1004            JsValue::from_str("not a function")
1005        })?;
1006        let result = match arg {
1007            Some(v) => func.call1(&btn, v),
1008            None => func.call0(&btn)
1009        };
1010        result.inspect_err(|_| logger::error(&format!("{name}.{method} call failed")))?;
1011        Ok(())
1012    }
1013
1014    fn bottom_button_property(&self, button: BottomButton, property: &str) -> Option<JsValue> {
1015        self.bottom_button_object(button)
1016            .ok()
1017            .and_then(|object| Reflect::get(&object, &property.into()).ok())
1018    }
1019
1020    /// Hide the on-screen keyboard.
1021    /// Call `WebApp.hideKeyboard()`.
1022    ///
1023    /// # Examples
1024    /// ```no_run
1025    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1026    /// # let app = TelegramWebApp::instance().unwrap();
1027    /// app.hide_keyboard().unwrap();
1028    /// ```
1029    ///
1030    /// # Errors
1031    /// Returns [`JsValue`] if the underlying JS call fails.
1032    pub fn hide_keyboard(&self) -> Result<(), JsValue> {
1033        self.call0("hideKeyboard")
1034    }
1035
1036    /// Call `WebApp.readTextFromClipboard(callback)`.
1037    ///
1038    /// # Examples
1039    /// ```no_run
1040    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1041    /// # let app = TelegramWebApp::instance().unwrap();
1042    /// app.read_text_from_clipboard(|text| {
1043    ///     let _ = text;
1044    /// })
1045    /// .unwrap();
1046    /// ```
1047    ///
1048    /// # Errors
1049    /// Returns [`JsValue`] if the underlying JS call fails.
1050    pub fn read_text_from_clipboard<F>(&self, callback: F) -> Result<(), JsValue>
1051    where
1052        F: 'static + Fn(String)
1053    {
1054        let cb = Closure::<dyn FnMut(JsValue)>::new(move |text: JsValue| {
1055            callback(text.as_string().unwrap_or_default());
1056        });
1057        let f = Reflect::get(&self.inner, &"readTextFromClipboard".into())?;
1058        let func = f
1059            .dyn_ref::<Function>()
1060            .ok_or_else(|| JsValue::from_str("readTextFromClipboard is not a function"))?;
1061        func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
1062        cb.forget();
1063        Ok(())
1064    }
1065
1066    /// Call `WebApp.MainButton.show()`.
1067    ///
1068    /// # Errors
1069    /// Returns [`JsValue`] if the underlying JS call fails.
1070    pub fn show_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
1071        self.bottom_button_method(button, "show", None)
1072    }
1073
1074    /// Hide a bottom button.
1075    ///
1076    /// # Errors
1077    /// Returns [`JsValue`] if the underlying JS call fails.
1078    pub fn hide_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
1079        self.bottom_button_method(button, "hide", None)
1080    }
1081
1082    /// Call `WebApp.ready()`.
1083    ///
1084    /// # Errors
1085    /// Returns [`JsValue`] if the underlying JS call fails.
1086    pub fn ready(&self) -> Result<(), JsValue> {
1087        self.call0("ready")
1088    }
1089
1090    /// Show back button.
1091    ///
1092    /// # Errors
1093    /// Returns [`JsValue`] if the underlying JS call fails.
1094    pub fn show_back_button(&self) -> Result<(), JsValue> {
1095        self.call_nested0("BackButton", "show")
1096    }
1097
1098    /// Hide back button.
1099    ///
1100    /// # Errors
1101    /// Returns [`JsValue`] if the underlying JS call fails.
1102    pub fn hide_back_button(&self) -> Result<(), JsValue> {
1103        self.call_nested0("BackButton", "hide")
1104    }
1105
1106    /// Call `WebApp.setHeaderColor(color)`.
1107    ///
1108    /// # Errors
1109    /// Returns [`JsValue`] if the underlying JS call fails.
1110    ///
1111    /// # Examples
1112    /// ```no_run
1113    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1114    /// # let app = TelegramWebApp::instance().unwrap();
1115    /// app.set_header_color("#ffffff").unwrap();
1116    /// ```
1117    pub fn set_header_color(&self, color: &str) -> Result<(), JsValue> {
1118        self.call1("setHeaderColor", &color.into())
1119    }
1120
1121    /// Call `WebApp.setBackgroundColor(color)`.
1122    ///
1123    /// # Errors
1124    /// Returns [`JsValue`] if the underlying JS call fails.
1125    ///
1126    /// # Examples
1127    /// ```no_run
1128    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1129    /// # let app = TelegramWebApp::instance().unwrap();
1130    /// app.set_background_color("#ffffff").unwrap();
1131    /// ```
1132    pub fn set_background_color(&self, color: &str) -> Result<(), JsValue> {
1133        self.call1("setBackgroundColor", &color.into())
1134    }
1135
1136    /// Call `WebApp.setBottomBarColor(color)`.
1137    ///
1138    /// # Errors
1139    /// Returns [`JsValue`] if the underlying JS call fails.
1140    ///
1141    /// # Examples
1142    /// ```no_run
1143    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1144    /// # let app = TelegramWebApp::instance().unwrap();
1145    /// app.set_bottom_bar_color("#ffffff").unwrap();
1146    /// ```
1147    pub fn set_bottom_bar_color(&self, color: &str) -> Result<(), JsValue> {
1148        self.call1("setBottomBarColor", &color.into())
1149    }
1150
1151    /// Set main button text.
1152    ///
1153    /// # Errors
1154    /// Returns [`JsValue`] if the underlying JS call fails.
1155    pub fn set_bottom_button_text(&self, button: BottomButton, text: &str) -> Result<(), JsValue> {
1156        self.bottom_button_method(button, "setText", Some(&text.into()))
1157    }
1158
1159    /// Set bottom button color (`setColor(color)`).
1160    ///
1161    /// # Errors
1162    /// Returns [`JsValue`] if the underlying JS call fails.
1163    ///
1164    /// # Examples
1165    /// ```no_run
1166    /// # use telegram_webapp_sdk::webapp::{TelegramWebApp, BottomButton};
1167    /// # let app = TelegramWebApp::instance().unwrap();
1168    /// let _ = app.set_bottom_button_color(BottomButton::Main, "#ff0000");
1169    /// ```
1170    pub fn set_bottom_button_color(
1171        &self,
1172        button: BottomButton,
1173        color: &str
1174    ) -> Result<(), JsValue> {
1175        self.bottom_button_method(button, "setColor", Some(&color.into()))
1176    }
1177
1178    /// Set bottom button text color (`setTextColor(color)`).
1179    ///
1180    /// # Errors
1181    /// Returns [`JsValue`] if the underlying JS call fails.
1182    ///
1183    /// # Examples
1184    /// ```no_run
1185    /// # use telegram_webapp_sdk::webapp::{TelegramWebApp, BottomButton};
1186    /// # let app = TelegramWebApp::instance().unwrap();
1187    /// let _ = app.set_bottom_button_text_color(BottomButton::Main, "#ffffff");
1188    /// ```
1189    pub fn set_bottom_button_text_color(
1190        &self,
1191        button: BottomButton,
1192        color: &str
1193    ) -> Result<(), JsValue> {
1194        self.bottom_button_method(button, "setTextColor", Some(&color.into()))
1195    }
1196
1197    /// Enable a bottom button, allowing user interaction.
1198    ///
1199    /// # Examples
1200    /// ```no_run
1201    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
1202    ///
1203    /// if let Some(app) = TelegramWebApp::instance() {
1204    ///     let _ = app.enable_bottom_button(BottomButton::Main);
1205    /// }
1206    /// ```
1207    pub fn enable_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
1208        self.bottom_button_method(button, "enable", None)
1209    }
1210
1211    /// Disable a bottom button, preventing user interaction.
1212    ///
1213    /// # Examples
1214    /// ```no_run
1215    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
1216    ///
1217    /// if let Some(app) = TelegramWebApp::instance() {
1218    ///     let _ = app.disable_bottom_button(BottomButton::Main);
1219    /// }
1220    /// ```
1221    pub fn disable_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
1222        self.bottom_button_method(button, "disable", None)
1223    }
1224
1225    /// Show the circular loading indicator on a bottom button.
1226    ///
1227    /// # Examples
1228    /// ```no_run
1229    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
1230    ///
1231    /// if let Some(app) = TelegramWebApp::instance() {
1232    ///     let _ = app.show_bottom_button_progress(BottomButton::Main, false);
1233    /// }
1234    /// ```
1235    pub fn show_bottom_button_progress(
1236        &self,
1237        button: BottomButton,
1238        leave_active: bool
1239    ) -> Result<(), JsValue> {
1240        let leave_active = JsValue::from_bool(leave_active);
1241        self.bottom_button_method(button, "showProgress", Some(&leave_active))
1242    }
1243
1244    /// Hide the loading indicator on a bottom button.
1245    ///
1246    /// # Examples
1247    /// ```no_run
1248    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
1249    ///
1250    /// if let Some(app) = TelegramWebApp::instance() {
1251    ///     let _ = app.hide_bottom_button_progress(BottomButton::Main);
1252    /// }
1253    /// ```
1254    pub fn hide_bottom_button_progress(&self, button: BottomButton) -> Result<(), JsValue> {
1255        self.bottom_button_method(button, "hideProgress", None)
1256    }
1257
1258    /// Returns whether the specified bottom button is currently visible.
1259    ///
1260    /// # Examples
1261    /// ```no_run
1262    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
1263    ///
1264    /// if let Some(app) = TelegramWebApp::instance() {
1265    ///     let _ = app.is_bottom_button_visible(BottomButton::Main);
1266    /// }
1267    /// ```
1268    pub fn is_bottom_button_visible(&self, button: BottomButton) -> bool {
1269        self.bottom_button_property(button, "isVisible")
1270            .and_then(|v| v.as_bool())
1271            .unwrap_or(false)
1272    }
1273
1274    /// Returns whether the specified bottom button is active (enabled).
1275    ///
1276    /// # Examples
1277    /// ```no_run
1278    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
1279    ///
1280    /// if let Some(app) = TelegramWebApp::instance() {
1281    ///     let _ = app.is_bottom_button_active(BottomButton::Main);
1282    /// }
1283    /// ```
1284    pub fn is_bottom_button_active(&self, button: BottomButton) -> bool {
1285        self.bottom_button_property(button, "isActive")
1286            .and_then(|v| v.as_bool())
1287            .unwrap_or(false)
1288    }
1289
1290    /// Returns whether the progress indicator is visible on the button.
1291    ///
1292    /// # Examples
1293    /// ```no_run
1294    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
1295    ///
1296    /// if let Some(app) = TelegramWebApp::instance() {
1297    ///     let _ = app.is_bottom_button_progress_visible(BottomButton::Main);
1298    /// }
1299    /// ```
1300    pub fn is_bottom_button_progress_visible(&self, button: BottomButton) -> bool {
1301        self.bottom_button_property(button, "isProgressVisible")
1302            .and_then(|v| v.as_bool())
1303            .unwrap_or(false)
1304    }
1305
1306    /// Returns the current text displayed on the button.
1307    ///
1308    /// # Examples
1309    /// ```no_run
1310    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
1311    ///
1312    /// if let Some(app) = TelegramWebApp::instance() {
1313    ///     let _ = app.bottom_button_text(BottomButton::Main);
1314    /// }
1315    /// ```
1316    pub fn bottom_button_text(&self, button: BottomButton) -> Option<String> {
1317        self.bottom_button_property(button, "text")?.as_string()
1318    }
1319
1320    /// Returns the current text color of the button.
1321    ///
1322    /// # Examples
1323    /// ```no_run
1324    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
1325    ///
1326    /// if let Some(app) = TelegramWebApp::instance() {
1327    ///     let _ = app.bottom_button_text_color(BottomButton::Main);
1328    /// }
1329    /// ```
1330    pub fn bottom_button_text_color(&self, button: BottomButton) -> Option<String> {
1331        self.bottom_button_property(button, "textColor")?
1332            .as_string()
1333    }
1334
1335    /// Returns the current background color of the button.
1336    ///
1337    /// # Examples
1338    /// ```no_run
1339    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
1340    ///
1341    /// if let Some(app) = TelegramWebApp::instance() {
1342    ///     let _ = app.bottom_button_color(BottomButton::Main);
1343    /// }
1344    /// ```
1345    pub fn bottom_button_color(&self, button: BottomButton) -> Option<String> {
1346        self.bottom_button_property(button, "color")?.as_string()
1347    }
1348
1349    /// Returns whether the shine effect is enabled on the button.
1350    ///
1351    /// # Examples
1352    /// ```no_run
1353    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
1354    ///
1355    /// if let Some(app) = TelegramWebApp::instance() {
1356    ///     let _ = app.bottom_button_has_shine_effect(BottomButton::Main);
1357    /// }
1358    /// ```
1359    pub fn bottom_button_has_shine_effect(&self, button: BottomButton) -> bool {
1360        self.bottom_button_property(button, "hasShineEffect")
1361            .and_then(|v| v.as_bool())
1362            .unwrap_or(false)
1363    }
1364
1365    /// Update bottom button state via `setParams`.
1366    ///
1367    /// # Examples
1368    /// ```no_run
1369    /// use telegram_webapp_sdk::webapp::{BottomButton, BottomButtonParams, TelegramWebApp};
1370    ///
1371    /// if let Some(app) = TelegramWebApp::instance() {
1372    ///     let params = BottomButtonParams {
1373    ///         text: Some("Send"),
1374    ///         ..Default::default()
1375    ///     };
1376    ///     let _ = app.set_bottom_button_params(BottomButton::Main, &params);
1377    /// }
1378    /// ```
1379    pub fn set_bottom_button_params(
1380        &self,
1381        button: BottomButton,
1382        params: &BottomButtonParams<'_>
1383    ) -> Result<(), JsValue> {
1384        let value = to_value(params).map_err(|err| JsValue::from_str(&err.to_string()))?;
1385        self.bottom_button_method(button, "setParams", Some(&value))
1386    }
1387
1388    /// Update secondary button state via `setParams`, including position.
1389    ///
1390    /// # Examples
1391    /// ```no_run
1392    /// use telegram_webapp_sdk::webapp::{
1393    ///     SecondaryButtonParams, SecondaryButtonPosition, TelegramWebApp
1394    /// };
1395    ///
1396    /// if let Some(app) = TelegramWebApp::instance() {
1397    ///     let params = SecondaryButtonParams {
1398    ///         position: Some(SecondaryButtonPosition::Left),
1399    ///         ..Default::default()
1400    ///     };
1401    ///     let _ = app.set_secondary_button_params(&params);
1402    /// }
1403    /// ```
1404    pub fn set_secondary_button_params(
1405        &self,
1406        params: &SecondaryButtonParams<'_>
1407    ) -> Result<(), JsValue> {
1408        let value = to_value(params).map_err(|err| JsValue::from_str(&err.to_string()))?;
1409        self.bottom_button_method(BottomButton::Secondary, "setParams", Some(&value))
1410    }
1411
1412    /// Returns the configured position of the secondary button, if available.
1413    ///
1414    /// # Examples
1415    /// ```no_run
1416    /// use telegram_webapp_sdk::webapp::{SecondaryButtonPosition, TelegramWebApp};
1417    ///
1418    /// if let Some(app) = TelegramWebApp::instance() {
1419    ///     let _ = app.secondary_button_position();
1420    /// }
1421    /// ```
1422    pub fn secondary_button_position(&self) -> Option<SecondaryButtonPosition> {
1423        self.bottom_button_property(BottomButton::Secondary, "position")
1424            .and_then(SecondaryButtonPosition::from_js_value)
1425    }
1426
1427    /// Set callback for `onClick()` on a bottom button.
1428    ///
1429    /// Returns an [`EventHandle`] that can be used to remove the callback.
1430    ///
1431    /// # Errors
1432    /// Returns [`JsValue`] if the underlying JS call fails.
1433    pub fn set_bottom_button_callback<F>(
1434        &self,
1435        button: BottomButton,
1436        callback: F
1437    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1438    where
1439        F: 'static + Fn()
1440    {
1441        let btn_val = Reflect::get(&self.inner, &button.js_name().into())?;
1442        let btn = btn_val.dyn_into::<Object>()?;
1443        let cb = Closure::<dyn FnMut()>::new(callback);
1444        let f = Reflect::get(&btn, &"onClick".into())?;
1445        let func = f
1446            .dyn_ref::<Function>()
1447            .ok_or_else(|| JsValue::from_str("onClick is not a function"))?;
1448        func.call1(&btn, cb.as_ref().unchecked_ref())?;
1449        Ok(EventHandle::new(btn, "offClick", None, cb))
1450    }
1451
1452    /// Remove previously set bottom button callback.
1453    ///
1454    /// # Errors
1455    /// Returns [`JsValue`] if the underlying JS call fails.
1456    pub fn remove_bottom_button_callback(
1457        &self,
1458        handle: EventHandle<dyn FnMut()>
1459    ) -> Result<(), JsValue> {
1460        handle.unregister()
1461    }
1462
1463    /// Legacy alias for [`Self::show_bottom_button`] with
1464    /// [`BottomButton::Main`].
1465    pub fn show_main_button(&self) -> Result<(), JsValue> {
1466        self.show_bottom_button(BottomButton::Main)
1467    }
1468
1469    /// Show the secondary bottom button.
1470    pub fn show_secondary_button(&self) -> Result<(), JsValue> {
1471        self.show_bottom_button(BottomButton::Secondary)
1472    }
1473
1474    /// Legacy alias for [`Self::hide_bottom_button`] with
1475    /// [`BottomButton::Main`].
1476    pub fn hide_main_button(&self) -> Result<(), JsValue> {
1477        self.hide_bottom_button(BottomButton::Main)
1478    }
1479
1480    /// Hide the secondary bottom button.
1481    pub fn hide_secondary_button(&self) -> Result<(), JsValue> {
1482        self.hide_bottom_button(BottomButton::Secondary)
1483    }
1484
1485    /// Legacy alias for [`Self::set_bottom_button_text`] with
1486    /// [`BottomButton::Main`].
1487    pub fn set_main_button_text(&self, text: &str) -> Result<(), JsValue> {
1488        self.set_bottom_button_text(BottomButton::Main, text)
1489    }
1490
1491    /// Set text for the secondary bottom button.
1492    pub fn set_secondary_button_text(&self, text: &str) -> Result<(), JsValue> {
1493        self.set_bottom_button_text(BottomButton::Secondary, text)
1494    }
1495
1496    /// Legacy alias for [`Self::set_bottom_button_color`] with
1497    /// [`BottomButton::Main`].
1498    pub fn set_main_button_color(&self, color: &str) -> Result<(), JsValue> {
1499        self.set_bottom_button_color(BottomButton::Main, color)
1500    }
1501
1502    /// Set color for the secondary bottom button.
1503    pub fn set_secondary_button_color(&self, color: &str) -> Result<(), JsValue> {
1504        self.set_bottom_button_color(BottomButton::Secondary, color)
1505    }
1506
1507    /// Legacy alias for [`Self::set_bottom_button_text_color`] with
1508    /// [`BottomButton::Main`].
1509    pub fn set_main_button_text_color(&self, color: &str) -> Result<(), JsValue> {
1510        self.set_bottom_button_text_color(BottomButton::Main, color)
1511    }
1512
1513    /// Set text color for the secondary bottom button.
1514    pub fn set_secondary_button_text_color(&self, color: &str) -> Result<(), JsValue> {
1515        self.set_bottom_button_text_color(BottomButton::Secondary, color)
1516    }
1517
1518    /// Enable the main bottom button.
1519    ///
1520    /// # Examples
1521    /// ```no_run
1522    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1523    ///
1524    /// if let Some(app) = TelegramWebApp::instance() {
1525    ///     let _ = app.enable_main_button();
1526    /// }
1527    /// ```
1528    pub fn enable_main_button(&self) -> Result<(), JsValue> {
1529        self.enable_bottom_button(BottomButton::Main)
1530    }
1531
1532    /// Enable the secondary bottom button.
1533    ///
1534    /// # Examples
1535    /// ```no_run
1536    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1537    ///
1538    /// if let Some(app) = TelegramWebApp::instance() {
1539    ///     let _ = app.enable_secondary_button();
1540    /// }
1541    /// ```
1542    pub fn enable_secondary_button(&self) -> Result<(), JsValue> {
1543        self.enable_bottom_button(BottomButton::Secondary)
1544    }
1545
1546    /// Disable the main bottom button.
1547    ///
1548    /// # Examples
1549    /// ```no_run
1550    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1551    ///
1552    /// if let Some(app) = TelegramWebApp::instance() {
1553    ///     let _ = app.disable_main_button();
1554    /// }
1555    /// ```
1556    pub fn disable_main_button(&self) -> Result<(), JsValue> {
1557        self.disable_bottom_button(BottomButton::Main)
1558    }
1559
1560    /// Disable the secondary bottom button.
1561    ///
1562    /// # Examples
1563    /// ```no_run
1564    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1565    ///
1566    /// if let Some(app) = TelegramWebApp::instance() {
1567    ///     let _ = app.disable_secondary_button();
1568    /// }
1569    /// ```
1570    pub fn disable_secondary_button(&self) -> Result<(), JsValue> {
1571        self.disable_bottom_button(BottomButton::Secondary)
1572    }
1573
1574    /// Show progress on the main bottom button.
1575    ///
1576    /// # Examples
1577    /// ```no_run
1578    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1579    ///
1580    /// if let Some(app) = TelegramWebApp::instance() {
1581    ///     let _ = app.show_main_button_progress(false);
1582    /// }
1583    /// ```
1584    pub fn show_main_button_progress(&self, leave_active: bool) -> Result<(), JsValue> {
1585        self.show_bottom_button_progress(BottomButton::Main, leave_active)
1586    }
1587
1588    /// Show progress on the secondary bottom button.
1589    ///
1590    /// # Examples
1591    /// ```no_run
1592    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1593    ///
1594    /// if let Some(app) = TelegramWebApp::instance() {
1595    ///     let _ = app.show_secondary_button_progress(false);
1596    /// }
1597    /// ```
1598    pub fn show_secondary_button_progress(&self, leave_active: bool) -> Result<(), JsValue> {
1599        self.show_bottom_button_progress(BottomButton::Secondary, leave_active)
1600    }
1601
1602    /// Hide progress indicator from the main bottom button.
1603    ///
1604    /// # Examples
1605    /// ```no_run
1606    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1607    ///
1608    /// if let Some(app) = TelegramWebApp::instance() {
1609    ///     let _ = app.hide_main_button_progress();
1610    /// }
1611    /// ```
1612    pub fn hide_main_button_progress(&self) -> Result<(), JsValue> {
1613        self.hide_bottom_button_progress(BottomButton::Main)
1614    }
1615
1616    /// Hide progress indicator from the secondary bottom button.
1617    ///
1618    /// # Examples
1619    /// ```no_run
1620    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1621    ///
1622    /// if let Some(app) = TelegramWebApp::instance() {
1623    ///     let _ = app.hide_secondary_button_progress();
1624    /// }
1625    /// ```
1626    pub fn hide_secondary_button_progress(&self) -> Result<(), JsValue> {
1627        self.hide_bottom_button_progress(BottomButton::Secondary)
1628    }
1629
1630    /// Update the main button state via
1631    /// [`set_bottom_button_params`](Self::set_bottom_button_params).
1632    pub fn set_main_button_params(&self, params: &BottomButtonParams<'_>) -> Result<(), JsValue> {
1633        self.set_bottom_button_params(BottomButton::Main, params)
1634    }
1635
1636    /// Legacy alias for [`Self::set_bottom_button_callback`] with
1637    /// [`BottomButton::Main`].
1638    pub fn set_main_button_callback<F>(
1639        &self,
1640        callback: F
1641    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1642    where
1643        F: 'static + Fn()
1644    {
1645        self.set_bottom_button_callback(BottomButton::Main, callback)
1646    }
1647
1648    /// Set callback for the secondary bottom button.
1649    pub fn set_secondary_button_callback<F>(
1650        &self,
1651        callback: F
1652    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1653    where
1654        F: 'static + Fn()
1655    {
1656        self.set_bottom_button_callback(BottomButton::Secondary, callback)
1657    }
1658
1659    /// Legacy alias for [`Self::remove_bottom_button_callback`].
1660    pub fn remove_main_button_callback(
1661        &self,
1662        handle: EventHandle<dyn FnMut()>
1663    ) -> Result<(), JsValue> {
1664        self.remove_bottom_button_callback(handle)
1665    }
1666
1667    /// Remove callback for the secondary bottom button.
1668    pub fn remove_secondary_button_callback(
1669        &self,
1670        handle: EventHandle<dyn FnMut()>
1671    ) -> Result<(), JsValue> {
1672        self.remove_bottom_button_callback(handle)
1673    }
1674
1675    /// Register event handler (`web_app_event_name`, callback).
1676    ///
1677    /// Returns an [`EventHandle`] that can be passed to
1678    /// [`off_event`](Self::off_event).
1679    ///
1680    /// # Errors
1681    /// Returns [`JsValue`] if the underlying JS call fails.
1682    pub fn on_event<F>(
1683        &self,
1684        event: &str,
1685        callback: F
1686    ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
1687    where
1688        F: 'static + Fn(JsValue)
1689    {
1690        let cb = Closure::<dyn FnMut(JsValue)>::new(callback);
1691        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1692        let func = f
1693            .dyn_ref::<Function>()
1694            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1695        func.call2(&self.inner, &event.into(), cb.as_ref().unchecked_ref())?;
1696        Ok(EventHandle::new(
1697            self.inner.clone(),
1698            "offEvent",
1699            Some(event.to_owned()),
1700            cb
1701        ))
1702    }
1703
1704    /// Register a callback for a background event.
1705    ///
1706    /// Returns an [`EventHandle`] that can be passed to
1707    /// [`off_event`](Self::off_event).
1708    ///
1709    /// # Errors
1710    /// Returns [`JsValue`] if the underlying JS call fails.
1711    pub fn on_background_event<F>(
1712        &self,
1713        event: BackgroundEvent,
1714        callback: F
1715    ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
1716    where
1717        F: 'static + Fn(JsValue)
1718    {
1719        let cb = Closure::<dyn FnMut(JsValue)>::new(callback);
1720        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1721        let func = f
1722            .dyn_ref::<Function>()
1723            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1724        func.call2(
1725            &self.inner,
1726            &event.as_str().into(),
1727            cb.as_ref().unchecked_ref()
1728        )?;
1729        Ok(EventHandle::new(
1730            self.inner.clone(),
1731            "offEvent",
1732            Some(event.as_str().to_string()),
1733            cb
1734        ))
1735    }
1736
1737    /// Deregister a previously registered event handler.
1738    ///
1739    /// # Errors
1740    /// Returns [`JsValue`] if the underlying JS call fails.
1741    pub fn off_event<T: ?Sized>(&self, handle: EventHandle<T>) -> Result<(), JsValue> {
1742        handle.unregister()
1743    }
1744
1745    /// Internal: call `this[field][method]()`
1746    fn call_nested0(&self, field: &str, method: &str) -> Result<(), JsValue> {
1747        let obj = Reflect::get(&self.inner, &field.into())?;
1748        let f = Reflect::get(&obj, &method.into())?;
1749        let func = f
1750            .dyn_ref::<Function>()
1751            .ok_or_else(|| JsValue::from_str("not a function"))?;
1752        func.call0(&obj)?;
1753        Ok(())
1754    }
1755
1756    // === Internal generic method helpers ===
1757
1758    fn call0(&self, method: &str) -> Result<(), JsValue> {
1759        let f = Reflect::get(&self.inner, &method.into())?;
1760        let func = f
1761            .dyn_ref::<Function>()
1762            .ok_or_else(|| JsValue::from_str("not a function"))?;
1763        func.call0(&self.inner)?;
1764        Ok(())
1765    }
1766
1767    fn call1(&self, method: &str, arg: &JsValue) -> Result<(), JsValue> {
1768        let f = Reflect::get(&self.inner, &method.into())?;
1769        let func = f
1770            .dyn_ref::<Function>()
1771            .ok_or_else(|| JsValue::from_str("not a function"))?;
1772        func.call1(&self.inner, arg)?;
1773        Ok(())
1774    }
1775
1776    /// Returns the current viewport height in pixels.
1777    ///
1778    /// # Examples
1779    /// ```no_run
1780    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1781    /// # let app = TelegramWebApp::instance().unwrap();
1782    /// let _ = app.viewport_height();
1783    /// ```
1784    pub fn viewport_height(&self) -> Option<f64> {
1785        Reflect::get(&self.inner, &"viewportHeight".into())
1786            .ok()?
1787            .as_f64()
1788    }
1789
1790    /// Returns the current viewport width in pixels.
1791    ///
1792    /// # Examples
1793    /// ```no_run
1794    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1795    /// # let app = TelegramWebApp::instance().unwrap();
1796    /// let _ = app.viewport_width();
1797    /// ```
1798    pub fn viewport_width(&self) -> Option<f64> {
1799        Reflect::get(&self.inner, &"viewportWidth".into())
1800            .ok()?
1801            .as_f64()
1802    }
1803
1804    /// Returns the stable viewport height in pixels.
1805    ///
1806    /// # Examples
1807    /// ```no_run
1808    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
1809    /// # let app = TelegramWebApp::instance().unwrap();
1810    /// let _ = app.viewport_stable_height();
1811    /// ```
1812    pub fn viewport_stable_height(&self) -> Option<f64> {
1813        Reflect::get(&self.inner, &"viewportStableHeight".into())
1814            .ok()?
1815            .as_f64()
1816    }
1817
1818    pub fn is_expanded(&self) -> bool {
1819        Reflect::get(&self.inner, &"isExpanded".into())
1820            .ok()
1821            .and_then(|v| v.as_bool())
1822            .unwrap_or(false)
1823    }
1824
1825    /// Returns whether the mini app is currently active (visible to the user).
1826    ///
1827    /// # Examples
1828    /// ```no_run
1829    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1830    ///
1831    /// if let Some(app) = TelegramWebApp::instance() {
1832    ///     let _ = app.is_active();
1833    /// }
1834    /// ```
1835    pub fn is_active(&self) -> bool {
1836        Reflect::get(&self.inner, &"isActive".into())
1837            .ok()
1838            .and_then(|v| v.as_bool())
1839            .unwrap_or(false)
1840    }
1841
1842    /// Returns whether the app is displayed in fullscreen mode.
1843    ///
1844    /// # Examples
1845    /// ```no_run
1846    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1847    ///
1848    /// if let Some(app) = TelegramWebApp::instance() {
1849    ///     let _ = app.is_fullscreen();
1850    /// }
1851    /// ```
1852    pub fn is_fullscreen(&self) -> bool {
1853        Reflect::get(&self.inner, &"isFullscreen".into())
1854            .ok()
1855            .and_then(|v| v.as_bool())
1856            .unwrap_or(false)
1857    }
1858
1859    /// Returns whether the orientation is locked.
1860    ///
1861    /// # Examples
1862    /// ```no_run
1863    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1864    ///
1865    /// if let Some(app) = TelegramWebApp::instance() {
1866    ///     let _ = app.is_orientation_locked();
1867    /// }
1868    /// ```
1869    pub fn is_orientation_locked(&self) -> bool {
1870        Reflect::get(&self.inner, &"isOrientationLocked".into())
1871            .ok()
1872            .and_then(|v| v.as_bool())
1873            .unwrap_or(false)
1874    }
1875
1876    /// Returns whether vertical swipes are currently enabled.
1877    ///
1878    /// # Examples
1879    /// ```no_run
1880    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1881    ///
1882    /// if let Some(app) = TelegramWebApp::instance() {
1883    ///     let _ = app.is_vertical_swipes_enabled();
1884    /// }
1885    /// ```
1886    pub fn is_vertical_swipes_enabled(&self) -> bool {
1887        Reflect::get(&self.inner, &"isVerticalSwipesEnabled".into())
1888            .ok()
1889            .and_then(|v| v.as_bool())
1890            .unwrap_or(false)
1891    }
1892
1893    fn safe_area_from_property(&self, property: &str) -> Option<SafeAreaInset> {
1894        let value = Reflect::get(&self.inner, &property.into()).ok()?;
1895        SafeAreaInset::from_js(value)
1896    }
1897
1898    /// Returns the safe area insets reported by Telegram.
1899    ///
1900    /// # Examples
1901    /// ```no_run
1902    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1903    ///
1904    /// if let Some(app) = TelegramWebApp::instance() {
1905    ///     let _ = app.safe_area_inset();
1906    /// }
1907    /// ```
1908    pub fn safe_area_inset(&self) -> Option<SafeAreaInset> {
1909        self.safe_area_from_property("safeAreaInset")
1910    }
1911
1912    /// Returns the content safe area insets reported by Telegram.
1913    ///
1914    /// # Examples
1915    /// ```no_run
1916    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
1917    ///
1918    /// if let Some(app) = TelegramWebApp::instance() {
1919    ///     let _ = app.content_safe_area_inset();
1920    /// }
1921    /// ```
1922    pub fn content_safe_area_inset(&self) -> Option<SafeAreaInset> {
1923        self.safe_area_from_property("contentSafeAreaInset")
1924    }
1925
1926    /// Call `WebApp.expand()` to expand the viewport.
1927    ///
1928    /// # Errors
1929    /// Returns [`JsValue`] if the underlying JS call fails.
1930    pub fn expand_viewport(&self) -> Result<(), JsValue> {
1931        self.call0("expand")
1932    }
1933
1934    /// Register a callback for theme changes.
1935    ///
1936    /// Returns an [`EventHandle`] that can be passed to
1937    /// [`off_event`](Self::off_event).
1938    ///
1939    /// # Errors
1940    /// Returns [`JsValue`] if the underlying JS call fails.
1941    pub fn on_theme_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
1942    where
1943        F: 'static + Fn()
1944    {
1945        let cb = Closure::<dyn FnMut()>::new(callback);
1946        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1947        let func = f
1948            .dyn_ref::<Function>()
1949            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1950        func.call2(
1951            &self.inner,
1952            &"themeChanged".into(),
1953            cb.as_ref().unchecked_ref()
1954        )?;
1955        Ok(EventHandle::new(
1956            self.inner.clone(),
1957            "offEvent",
1958            Some("themeChanged".to_string()),
1959            cb
1960        ))
1961    }
1962
1963    /// Register a callback for safe area changes.
1964    ///
1965    /// Returns an [`EventHandle`] that can be passed to
1966    /// [`off_event`](Self::off_event).
1967    ///
1968    /// # Errors
1969    /// Returns [`JsValue`] if the underlying JS call fails.
1970    pub fn on_safe_area_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
1971    where
1972        F: 'static + Fn()
1973    {
1974        let cb = Closure::<dyn FnMut()>::new(callback);
1975        let f = Reflect::get(&self.inner, &"onEvent".into())?;
1976        let func = f
1977            .dyn_ref::<Function>()
1978            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1979        func.call2(
1980            &self.inner,
1981            &"safeAreaChanged".into(),
1982            cb.as_ref().unchecked_ref()
1983        )?;
1984        Ok(EventHandle::new(
1985            self.inner.clone(),
1986            "offEvent",
1987            Some("safeAreaChanged".to_string()),
1988            cb
1989        ))
1990    }
1991
1992    /// Register a callback for content safe area changes.
1993    ///
1994    /// Returns an [`EventHandle`] that can be passed to
1995    /// [`off_event`](Self::off_event).
1996    ///
1997    /// # Errors
1998    /// Returns [`JsValue`] if the underlying JS call fails.
1999    pub fn on_content_safe_area_changed<F>(
2000        &self,
2001        callback: F
2002    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
2003    where
2004        F: 'static + Fn()
2005    {
2006        let cb = Closure::<dyn FnMut()>::new(callback);
2007        let f = Reflect::get(&self.inner, &"onEvent".into())?;
2008        let func = f
2009            .dyn_ref::<Function>()
2010            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
2011        func.call2(
2012            &self.inner,
2013            &"contentSafeAreaChanged".into(),
2014            cb.as_ref().unchecked_ref()
2015        )?;
2016        Ok(EventHandle::new(
2017            self.inner.clone(),
2018            "offEvent",
2019            Some("contentSafeAreaChanged".to_string()),
2020            cb
2021        ))
2022    }
2023
2024    /// Register a callback for viewport changes.
2025    ///
2026    /// Returns an [`EventHandle`] that can be passed to
2027    /// [`off_event`](Self::off_event).
2028    ///
2029    /// # Errors
2030    /// Returns [`JsValue`] if the underlying JS call fails.
2031    pub fn on_viewport_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
2032    where
2033        F: 'static + Fn()
2034    {
2035        let cb = Closure::<dyn FnMut()>::new(callback);
2036        let f = Reflect::get(&self.inner, &"onEvent".into())?;
2037        let func = f
2038            .dyn_ref::<Function>()
2039            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
2040        func.call2(
2041            &self.inner,
2042            &"viewportChanged".into(),
2043            cb.as_ref().unchecked_ref()
2044        )?;
2045        Ok(EventHandle::new(
2046            self.inner.clone(),
2047            "offEvent",
2048            Some("viewportChanged".to_string()),
2049            cb
2050        ))
2051    }
2052
2053    /// Register a callback for received clipboard text.
2054    ///
2055    /// Returns an [`EventHandle`] that can be passed to
2056    /// [`off_event`](Self::off_event).
2057    ///
2058    /// # Errors
2059    /// Returns [`JsValue`] if the underlying JS call fails.
2060    pub fn on_clipboard_text_received<F>(
2061        &self,
2062        callback: F
2063    ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
2064    where
2065        F: 'static + Fn(String)
2066    {
2067        let cb = Closure::<dyn FnMut(JsValue)>::new(move |text: JsValue| {
2068            callback(text.as_string().unwrap_or_default());
2069        });
2070        let f = Reflect::get(&self.inner, &"onEvent".into())?;
2071        let func = f
2072            .dyn_ref::<Function>()
2073            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
2074        func.call2(
2075            &self.inner,
2076            &"clipboardTextReceived".into(),
2077            cb.as_ref().unchecked_ref()
2078        )?;
2079        Ok(EventHandle::new(
2080            self.inner.clone(),
2081            "offEvent",
2082            Some("clipboardTextReceived".to_string()),
2083            cb
2084        ))
2085    }
2086
2087    /// Register a callback for invoice payment result.
2088    ///
2089    /// Returns an [`EventHandle`] that can be passed to
2090    /// [`off_event`](Self::off_event).
2091    ///
2092    /// # Examples
2093    /// ```no_run
2094    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
2095    /// # let app = TelegramWebApp::instance().unwrap();
2096    /// let handle = app
2097    ///     .on_invoice_closed(|status| {
2098    ///         let _ = status;
2099    ///     })
2100    ///     .unwrap();
2101    /// app.off_event(handle).unwrap();
2102    /// ```
2103    ///
2104    /// # Errors
2105    /// Returns [`JsValue`] if the underlying JS call fails.
2106    pub fn on_invoice_closed<F>(
2107        &self,
2108        callback: F
2109    ) -> Result<EventHandle<dyn FnMut(String)>, JsValue>
2110    where
2111        F: 'static + Fn(String)
2112    {
2113        let cb = Closure::<dyn FnMut(String)>::new(callback);
2114        let f = Reflect::get(&self.inner, &"onEvent".into())?;
2115        let func = f
2116            .dyn_ref::<Function>()
2117            .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
2118        func.call2(
2119            &self.inner,
2120            &"invoiceClosed".into(),
2121            cb.as_ref().unchecked_ref()
2122        )?;
2123        Ok(EventHandle::new(
2124            self.inner.clone(),
2125            "offEvent",
2126            Some("invoiceClosed".to_string()),
2127            cb
2128        ))
2129    }
2130
2131    /// Registers a callback for the native back button.
2132    ///
2133    /// Returns an [`EventHandle`] that can be passed to
2134    /// [`remove_back_button_callback`](Self::remove_back_button_callback).
2135    ///
2136    /// # Examples
2137    /// ```no_run
2138    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
2139    /// # let app = TelegramWebApp::instance().unwrap();
2140    /// let handle = app.set_back_button_callback(|| {}).expect("callback");
2141    /// app.remove_back_button_callback(handle).unwrap();
2142    /// ```
2143    ///
2144    /// # Errors
2145    /// Returns [`JsValue`] if the underlying JS call fails.
2146    pub fn set_back_button_callback<F>(
2147        &self,
2148        callback: F
2149    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
2150    where
2151        F: 'static + Fn()
2152    {
2153        let back_button_val = Reflect::get(&self.inner, &"BackButton".into())?;
2154        let back_button = back_button_val.dyn_into::<Object>()?;
2155        let cb = Closure::<dyn FnMut()>::new(callback);
2156        let f = Reflect::get(&back_button, &"onClick".into())?;
2157        let func = f
2158            .dyn_ref::<Function>()
2159            .ok_or_else(|| JsValue::from_str("onClick is not a function"))?;
2160        func.call1(&back_button, cb.as_ref().unchecked_ref())?;
2161        Ok(EventHandle::new(back_button, "offClick", None, cb))
2162    }
2163
2164    /// Remove previously set back button callback.
2165    ///
2166    /// # Errors
2167    /// Returns [`JsValue`] if the underlying JS call fails.
2168    pub fn remove_back_button_callback(
2169        &self,
2170        handle: EventHandle<dyn FnMut()>
2171    ) -> Result<(), JsValue> {
2172        handle.unregister()
2173    }
2174    /// Returns whether the native back button is visible.
2175    ///
2176    /// # Examples
2177    /// ```no_run
2178    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
2179    /// # let app = TelegramWebApp::instance().unwrap();
2180    /// let _ = app.is_back_button_visible();
2181    /// ```
2182    pub fn is_back_button_visible(&self) -> bool {
2183        Reflect::get(&self.inner, &"BackButton".into())
2184            .ok()
2185            .and_then(|bb| Reflect::get(&bb, &"isVisible".into()).ok())
2186            .and_then(|v| v.as_bool())
2187            .unwrap_or(false)
2188    }
2189}
2190
2191#[cfg(test)]
2192mod tests {
2193    use std::{
2194        cell::{Cell, RefCell},
2195        rc::Rc
2196    };
2197
2198    use js_sys::{Function, Object, Reflect};
2199    use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
2200    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
2201    use web_sys::window;
2202
2203    use super::*;
2204
2205    wasm_bindgen_test_configure!(run_in_browser);
2206
2207    #[allow(dead_code)]
2208    fn setup_webapp() -> Object {
2209        let win = window().unwrap();
2210        let telegram = Object::new();
2211        let webapp = Object::new();
2212        let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
2213        let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
2214        webapp
2215    }
2216
2217    #[wasm_bindgen_test]
2218    #[allow(dead_code, clippy::unused_unit)]
2219    fn hide_keyboard_calls_js() {
2220        let webapp = setup_webapp();
2221        let called = Rc::new(Cell::new(false));
2222        let called_clone = Rc::clone(&called);
2223
2224        let hide_cb = Closure::<dyn FnMut()>::new(move || {
2225            called_clone.set(true);
2226        });
2227        let _ = Reflect::set(
2228            &webapp,
2229            &"hideKeyboard".into(),
2230            hide_cb.as_ref().unchecked_ref()
2231        );
2232        hide_cb.forget();
2233
2234        let app = TelegramWebApp::instance().unwrap();
2235        app.hide_keyboard().unwrap();
2236        assert!(called.get());
2237    }
2238
2239    #[wasm_bindgen_test]
2240    #[allow(dead_code, clippy::unused_unit)]
2241    fn hide_main_button_calls_js() {
2242        let webapp = setup_webapp();
2243        let main_button = Object::new();
2244        let called = Rc::new(Cell::new(false));
2245        let called_clone = Rc::clone(&called);
2246
2247        let hide_cb = Closure::<dyn FnMut()>::new(move || {
2248            called_clone.set(true);
2249        });
2250        let _ = Reflect::set(
2251            &main_button,
2252            &"hide".into(),
2253            hide_cb.as_ref().unchecked_ref()
2254        );
2255        hide_cb.forget();
2256
2257        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
2258
2259        let app = TelegramWebApp::instance().unwrap();
2260        app.hide_bottom_button(BottomButton::Main).unwrap();
2261        assert!(called.get());
2262    }
2263
2264    #[wasm_bindgen_test]
2265    #[allow(dead_code, clippy::unused_unit)]
2266    fn hide_secondary_button_calls_js() {
2267        let webapp = setup_webapp();
2268        let secondary_button = Object::new();
2269        let called = Rc::new(Cell::new(false));
2270        let called_clone = Rc::clone(&called);
2271
2272        let hide_cb = Closure::<dyn FnMut()>::new(move || {
2273            called_clone.set(true);
2274        });
2275        let _ = Reflect::set(
2276            &secondary_button,
2277            &"hide".into(),
2278            hide_cb.as_ref().unchecked_ref()
2279        );
2280        hide_cb.forget();
2281
2282        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
2283
2284        let app = TelegramWebApp::instance().unwrap();
2285        app.hide_bottom_button(BottomButton::Secondary).unwrap();
2286        assert!(called.get());
2287    }
2288
2289    #[wasm_bindgen_test]
2290    #[allow(dead_code, clippy::unused_unit)]
2291    fn set_bottom_button_color_calls_js() {
2292        let webapp = setup_webapp();
2293        let main_button = Object::new();
2294        let received = Rc::new(RefCell::new(None));
2295        let rc_clone = Rc::clone(&received);
2296
2297        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2298            *rc_clone.borrow_mut() = v.as_string();
2299        });
2300        let _ = Reflect::set(
2301            &main_button,
2302            &"setColor".into(),
2303            set_color_cb.as_ref().unchecked_ref()
2304        );
2305        set_color_cb.forget();
2306
2307        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
2308
2309        let app = TelegramWebApp::instance().unwrap();
2310        app.set_bottom_button_color(BottomButton::Main, "#00ff00")
2311            .unwrap();
2312        assert_eq!(received.borrow().as_deref(), Some("#00ff00"));
2313    }
2314
2315    #[wasm_bindgen_test]
2316    #[allow(dead_code, clippy::unused_unit)]
2317    fn set_secondary_button_color_calls_js() {
2318        let webapp = setup_webapp();
2319        let secondary_button = Object::new();
2320        let received = Rc::new(RefCell::new(None));
2321        let rc_clone = Rc::clone(&received);
2322
2323        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2324            *rc_clone.borrow_mut() = v.as_string();
2325        });
2326        let _ = Reflect::set(
2327            &secondary_button,
2328            &"setColor".into(),
2329            set_color_cb.as_ref().unchecked_ref()
2330        );
2331        set_color_cb.forget();
2332
2333        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
2334
2335        let app = TelegramWebApp::instance().unwrap();
2336        app.set_bottom_button_color(BottomButton::Secondary, "#00ff00")
2337            .unwrap();
2338        assert_eq!(received.borrow().as_deref(), Some("#00ff00"));
2339    }
2340
2341    #[wasm_bindgen_test]
2342    #[allow(dead_code, clippy::unused_unit)]
2343    fn set_bottom_button_text_color_calls_js() {
2344        let webapp = setup_webapp();
2345        let main_button = Object::new();
2346        let received = Rc::new(RefCell::new(None));
2347        let rc_clone = Rc::clone(&received);
2348
2349        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2350            *rc_clone.borrow_mut() = v.as_string();
2351        });
2352        let _ = Reflect::set(
2353            &main_button,
2354            &"setTextColor".into(),
2355            set_color_cb.as_ref().unchecked_ref()
2356        );
2357        set_color_cb.forget();
2358
2359        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
2360
2361        let app = TelegramWebApp::instance().unwrap();
2362        app.set_bottom_button_text_color(BottomButton::Main, "#112233")
2363            .unwrap();
2364        assert_eq!(received.borrow().as_deref(), Some("#112233"));
2365    }
2366
2367    #[wasm_bindgen_test]
2368    #[allow(dead_code, clippy::unused_unit)]
2369    fn set_secondary_button_text_color_calls_js() {
2370        let webapp = setup_webapp();
2371        let secondary_button = Object::new();
2372        let received = Rc::new(RefCell::new(None));
2373        let rc_clone = Rc::clone(&received);
2374
2375        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2376            *rc_clone.borrow_mut() = v.as_string();
2377        });
2378        let _ = Reflect::set(
2379            &secondary_button,
2380            &"setTextColor".into(),
2381            set_color_cb.as_ref().unchecked_ref()
2382        );
2383        set_color_cb.forget();
2384
2385        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
2386
2387        let app = TelegramWebApp::instance().unwrap();
2388        app.set_bottom_button_text_color(BottomButton::Secondary, "#112233")
2389            .unwrap();
2390        assert_eq!(received.borrow().as_deref(), Some("#112233"));
2391    }
2392
2393    #[wasm_bindgen_test]
2394    #[allow(dead_code, clippy::unused_unit)]
2395    fn enable_bottom_button_calls_js() {
2396        let webapp = setup_webapp();
2397        let button = Object::new();
2398        let called = Rc::new(Cell::new(false));
2399        let called_clone = Rc::clone(&called);
2400
2401        let enable_cb = Closure::<dyn FnMut()>::new(move || {
2402            called_clone.set(true);
2403        });
2404        let _ = Reflect::set(
2405            &button,
2406            &"enable".into(),
2407            enable_cb.as_ref().unchecked_ref()
2408        );
2409        enable_cb.forget();
2410
2411        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
2412
2413        let app = TelegramWebApp::instance().unwrap();
2414        app.enable_bottom_button(BottomButton::Main).unwrap();
2415        assert!(called.get());
2416    }
2417
2418    #[wasm_bindgen_test]
2419    #[allow(dead_code, clippy::unused_unit)]
2420    fn show_bottom_button_progress_passes_flag() {
2421        let webapp = setup_webapp();
2422        let button = Object::new();
2423        let received = Rc::new(RefCell::new(None));
2424        let rc_clone = Rc::clone(&received);
2425
2426        let cb = Closure::<dyn FnMut(JsValue)>::new(move |arg: JsValue| {
2427            *rc_clone.borrow_mut() = arg.as_bool();
2428        });
2429        let _ = Reflect::set(&button, &"showProgress".into(), cb.as_ref().unchecked_ref());
2430        cb.forget();
2431
2432        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
2433
2434        let app = TelegramWebApp::instance().unwrap();
2435        app.show_bottom_button_progress(BottomButton::Main, true)
2436            .unwrap();
2437        assert_eq!(*received.borrow(), Some(true));
2438    }
2439
2440    #[wasm_bindgen_test]
2441    #[allow(dead_code, clippy::unused_unit)]
2442    fn set_bottom_button_params_serializes() {
2443        let webapp = setup_webapp();
2444        let button = Object::new();
2445        let received = Rc::new(RefCell::new(Object::new()));
2446        let rc_clone = Rc::clone(&received);
2447
2448        let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
2449            let obj = value.dyn_into::<Object>().expect("object");
2450            rc_clone.replace(obj);
2451        });
2452        let _ = Reflect::set(&button, &"setParams".into(), cb.as_ref().unchecked_ref());
2453        cb.forget();
2454
2455        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
2456
2457        let app = TelegramWebApp::instance().unwrap();
2458        let params = BottomButtonParams {
2459            text:             Some("Send"),
2460            color:            Some("#ffffff"),
2461            text_color:       Some("#000000"),
2462            is_active:        Some(true),
2463            is_visible:       Some(true),
2464            has_shine_effect: Some(false)
2465        };
2466        app.set_bottom_button_params(BottomButton::Main, &params)
2467            .unwrap();
2468
2469        let stored = received.borrow();
2470        assert_eq!(
2471            Reflect::get(&stored, &"text".into()).unwrap().as_string(),
2472            Some("Send".to_string())
2473        );
2474        assert_eq!(
2475            Reflect::get(&stored, &"color".into()).unwrap().as_string(),
2476            Some("#ffffff".to_string())
2477        );
2478        assert_eq!(
2479            Reflect::get(&stored, &"text_color".into())
2480                .unwrap()
2481                .as_string(),
2482            Some("#000000".to_string())
2483        );
2484    }
2485
2486    #[wasm_bindgen_test]
2487    #[allow(dead_code, clippy::unused_unit)]
2488    fn set_secondary_button_params_serializes_position() {
2489        let webapp = setup_webapp();
2490        let button = Object::new();
2491        let received = Rc::new(RefCell::new(Object::new()));
2492        let rc_clone = Rc::clone(&received);
2493
2494        let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
2495            let obj = value.dyn_into::<Object>().expect("object");
2496            rc_clone.replace(obj);
2497        });
2498        let _ = Reflect::set(&button, &"setParams".into(), cb.as_ref().unchecked_ref());
2499        cb.forget();
2500
2501        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &button);
2502
2503        let app = TelegramWebApp::instance().unwrap();
2504        let params = SecondaryButtonParams {
2505            common:   BottomButtonParams {
2506                text: Some("Next"),
2507                ..Default::default()
2508            },
2509            position: Some(SecondaryButtonPosition::Left)
2510        };
2511        app.set_secondary_button_params(&params).unwrap();
2512
2513        let stored = received.borrow();
2514        assert_eq!(
2515            Reflect::get(&stored, &"text".into()).unwrap().as_string(),
2516            Some("Next".to_string())
2517        );
2518        assert_eq!(
2519            Reflect::get(&stored, &"position".into())
2520                .unwrap()
2521                .as_string(),
2522            Some("left".to_string())
2523        );
2524    }
2525
2526    #[wasm_bindgen_test]
2527    #[allow(dead_code, clippy::unused_unit)]
2528    fn bottom_button_getters_return_values() {
2529        let webapp = setup_webapp();
2530        let button = Object::new();
2531        let _ = Reflect::set(&button, &"text".into(), &"Label".into());
2532        let _ = Reflect::set(&button, &"textColor".into(), &"#111111".into());
2533        let _ = Reflect::set(&button, &"color".into(), &"#222222".into());
2534        let _ = Reflect::set(&button, &"isVisible".into(), &JsValue::TRUE);
2535        let _ = Reflect::set(&button, &"isActive".into(), &JsValue::TRUE);
2536        let _ = Reflect::set(&button, &"isProgressVisible".into(), &JsValue::FALSE);
2537        let _ = Reflect::set(&button, &"hasShineEffect".into(), &JsValue::TRUE);
2538
2539        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
2540
2541        let app = TelegramWebApp::instance().unwrap();
2542        assert_eq!(
2543            app.bottom_button_text(BottomButton::Main),
2544            Some("Label".into())
2545        );
2546        assert_eq!(
2547            app.bottom_button_text_color(BottomButton::Main),
2548            Some("#111111".into())
2549        );
2550        assert_eq!(
2551            app.bottom_button_color(BottomButton::Main),
2552            Some("#222222".into())
2553        );
2554        assert!(app.is_bottom_button_visible(BottomButton::Main));
2555        assert!(app.is_bottom_button_active(BottomButton::Main));
2556        assert!(!app.is_bottom_button_progress_visible(BottomButton::Main));
2557        assert!(app.bottom_button_has_shine_effect(BottomButton::Main));
2558    }
2559
2560    #[wasm_bindgen_test]
2561    #[allow(dead_code, clippy::unused_unit)]
2562    fn secondary_button_position_is_parsed() {
2563        let webapp = setup_webapp();
2564        let button = Object::new();
2565        let _ = Reflect::set(&button, &"position".into(), &"right".into());
2566        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &button);
2567
2568        let app = TelegramWebApp::instance().unwrap();
2569        assert_eq!(
2570            app.secondary_button_position(),
2571            Some(SecondaryButtonPosition::Right)
2572        );
2573    }
2574
2575    #[wasm_bindgen_test]
2576    #[allow(dead_code, clippy::unused_unit)]
2577    fn set_header_color_calls_js() {
2578        let webapp = setup_webapp();
2579        let received = Rc::new(RefCell::new(None));
2580        let rc_clone = Rc::clone(&received);
2581
2582        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2583            *rc_clone.borrow_mut() = v.as_string();
2584        });
2585        let _ = Reflect::set(
2586            &webapp,
2587            &"setHeaderColor".into(),
2588            cb.as_ref().unchecked_ref()
2589        );
2590        cb.forget();
2591
2592        let app = TelegramWebApp::instance().unwrap();
2593        app.set_header_color("#abcdef").unwrap();
2594        assert_eq!(received.borrow().as_deref(), Some("#abcdef"));
2595    }
2596
2597    #[wasm_bindgen_test]
2598    #[allow(dead_code, clippy::unused_unit)]
2599    fn set_background_color_calls_js() {
2600        let webapp = setup_webapp();
2601        let received = Rc::new(RefCell::new(None));
2602        let rc_clone = Rc::clone(&received);
2603
2604        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2605            *rc_clone.borrow_mut() = v.as_string();
2606        });
2607        let _ = Reflect::set(
2608            &webapp,
2609            &"setBackgroundColor".into(),
2610            cb.as_ref().unchecked_ref()
2611        );
2612        cb.forget();
2613
2614        let app = TelegramWebApp::instance().unwrap();
2615        app.set_background_color("#123456").unwrap();
2616        assert_eq!(received.borrow().as_deref(), Some("#123456"));
2617    }
2618
2619    #[wasm_bindgen_test]
2620    #[allow(dead_code, clippy::unused_unit)]
2621    fn set_bottom_bar_color_calls_js() {
2622        let webapp = setup_webapp();
2623        let received = Rc::new(RefCell::new(None));
2624        let rc_clone = Rc::clone(&received);
2625
2626        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2627            *rc_clone.borrow_mut() = v.as_string();
2628        });
2629        let _ = Reflect::set(
2630            &webapp,
2631            &"setBottomBarColor".into(),
2632            cb.as_ref().unchecked_ref()
2633        );
2634        cb.forget();
2635
2636        let app = TelegramWebApp::instance().unwrap();
2637        app.set_bottom_bar_color("#654321").unwrap();
2638        assert_eq!(received.borrow().as_deref(), Some("#654321"));
2639    }
2640
2641    #[wasm_bindgen_test]
2642    #[allow(dead_code, clippy::unused_unit)]
2643    fn viewport_dimensions() {
2644        let webapp = setup_webapp();
2645        let _ = Reflect::set(&webapp, &"viewportWidth".into(), &JsValue::from_f64(320.0));
2646        let _ = Reflect::set(
2647            &webapp,
2648            &"viewportStableHeight".into(),
2649            &JsValue::from_f64(480.0)
2650        );
2651        let app = TelegramWebApp::instance().unwrap();
2652        assert_eq!(app.viewport_width(), Some(320.0));
2653        assert_eq!(app.viewport_stable_height(), Some(480.0));
2654    }
2655
2656    #[wasm_bindgen_test]
2657    #[allow(dead_code, clippy::unused_unit)]
2658    fn version_check_invokes_js() {
2659        let webapp = setup_webapp();
2660        let cb = Function::new_with_args("v", "return v === '9.0';");
2661        let _ = Reflect::set(&webapp, &"isVersionAtLeast".into(), &cb);
2662
2663        let app = TelegramWebApp::instance().unwrap();
2664        assert!(app.is_version_at_least("9.0").unwrap());
2665        assert!(!app.is_version_at_least("9.1").unwrap());
2666    }
2667
2668    #[wasm_bindgen_test]
2669    #[allow(dead_code, clippy::unused_unit)]
2670    fn safe_area_insets_are_parsed() {
2671        let webapp = setup_webapp();
2672        let safe_area = Object::new();
2673        let _ = Reflect::set(&safe_area, &"top".into(), &JsValue::from_f64(1.0));
2674        let _ = Reflect::set(&safe_area, &"bottom".into(), &JsValue::from_f64(2.0));
2675        let _ = Reflect::set(&safe_area, &"left".into(), &JsValue::from_f64(3.0));
2676        let _ = Reflect::set(&safe_area, &"right".into(), &JsValue::from_f64(4.0));
2677        let _ = Reflect::set(&webapp, &"safeAreaInset".into(), &safe_area);
2678
2679        let content_safe = Object::new();
2680        let _ = Reflect::set(&content_safe, &"top".into(), &JsValue::from_f64(5.0));
2681        let _ = Reflect::set(&content_safe, &"bottom".into(), &JsValue::from_f64(6.0));
2682        let _ = Reflect::set(&content_safe, &"left".into(), &JsValue::from_f64(7.0));
2683        let _ = Reflect::set(&content_safe, &"right".into(), &JsValue::from_f64(8.0));
2684        let _ = Reflect::set(&webapp, &"contentSafeAreaInset".into(), &content_safe);
2685
2686        let app = TelegramWebApp::instance().unwrap();
2687        let inset = app.safe_area_inset().expect("safe area");
2688        assert_eq!(inset.top, 1.0);
2689        assert_eq!(inset.bottom, 2.0);
2690        assert_eq!(inset.left, 3.0);
2691        assert_eq!(inset.right, 4.0);
2692
2693        let content = app.content_safe_area_inset().expect("content area");
2694        assert_eq!(content.top, 5.0);
2695    }
2696
2697    #[wasm_bindgen_test]
2698    #[allow(dead_code, clippy::unused_unit)]
2699    fn activity_flags_are_reported() {
2700        let webapp = setup_webapp();
2701        let _ = Reflect::set(&webapp, &"isActive".into(), &JsValue::TRUE);
2702        let _ = Reflect::set(&webapp, &"isFullscreen".into(), &JsValue::TRUE);
2703        let _ = Reflect::set(&webapp, &"isOrientationLocked".into(), &JsValue::FALSE);
2704        let _ = Reflect::set(&webapp, &"isVerticalSwipesEnabled".into(), &JsValue::TRUE);
2705
2706        let app = TelegramWebApp::instance().unwrap();
2707        assert!(app.is_active());
2708        assert!(app.is_fullscreen());
2709        assert!(!app.is_orientation_locked());
2710        assert!(app.is_vertical_swipes_enabled());
2711    }
2712
2713    #[wasm_bindgen_test]
2714    #[allow(dead_code, clippy::unused_unit)]
2715    fn back_button_visibility_and_callback() {
2716        let webapp = setup_webapp();
2717        let back_button = Object::new();
2718        let _ = Reflect::set(&webapp, &"BackButton".into(), &back_button);
2719        let _ = Reflect::set(&back_button, &"isVisible".into(), &JsValue::TRUE);
2720
2721        let on_click = Function::new_with_args("cb", "this.cb = cb;");
2722        let off_click = Function::new_with_args("", "delete this.cb;");
2723        let _ = Reflect::set(&back_button, &"onClick".into(), &on_click);
2724        let _ = Reflect::set(&back_button, &"offClick".into(), &off_click);
2725
2726        let called = Rc::new(Cell::new(false));
2727        let called_clone = Rc::clone(&called);
2728
2729        let app = TelegramWebApp::instance().unwrap();
2730        assert!(app.is_back_button_visible());
2731        let handle = app
2732            .set_back_button_callback(move || {
2733                called_clone.set(true);
2734            })
2735            .unwrap();
2736
2737        let stored = Reflect::has(&back_button, &"cb".into()).unwrap();
2738        assert!(stored);
2739
2740        let cb_fn = Reflect::get(&back_button, &"cb".into())
2741            .unwrap()
2742            .dyn_into::<Function>()
2743            .unwrap();
2744        let _ = cb_fn.call0(&JsValue::NULL);
2745        assert!(called.get());
2746
2747        app.remove_back_button_callback(handle).unwrap();
2748        let stored_after = Reflect::has(&back_button, &"cb".into()).unwrap();
2749        assert!(!stored_after);
2750    }
2751
2752    #[wasm_bindgen_test]
2753    #[allow(dead_code, clippy::unused_unit)]
2754    fn bottom_button_callback_register_and_remove() {
2755        let webapp = setup_webapp();
2756        let main_button = Object::new();
2757        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
2758
2759        let on_click = Function::new_with_args("cb", "this.cb = cb;");
2760        let off_click = Function::new_with_args("", "delete this.cb;");
2761        let _ = Reflect::set(&main_button, &"onClick".into(), &on_click);
2762        let _ = Reflect::set(&main_button, &"offClick".into(), &off_click);
2763
2764        let called = Rc::new(Cell::new(false));
2765        let called_clone = Rc::clone(&called);
2766
2767        let app = TelegramWebApp::instance().unwrap();
2768        let handle = app
2769            .set_bottom_button_callback(BottomButton::Main, move || {
2770                called_clone.set(true);
2771            })
2772            .unwrap();
2773
2774        let stored = Reflect::has(&main_button, &"cb".into()).unwrap();
2775        assert!(stored);
2776
2777        let cb_fn = Reflect::get(&main_button, &"cb".into())
2778            .unwrap()
2779            .dyn_into::<Function>()
2780            .unwrap();
2781        let _ = cb_fn.call0(&JsValue::NULL);
2782        assert!(called.get());
2783
2784        app.remove_bottom_button_callback(handle).unwrap();
2785        let stored_after = Reflect::has(&main_button, &"cb".into()).unwrap();
2786        assert!(!stored_after);
2787    }
2788
2789    #[wasm_bindgen_test]
2790    #[allow(dead_code, clippy::unused_unit)]
2791    fn secondary_button_callback_register_and_remove() {
2792        let webapp = setup_webapp();
2793        let secondary_button = Object::new();
2794        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
2795
2796        let on_click = Function::new_with_args("cb", "this.cb = cb;");
2797        let off_click = Function::new_with_args("", "delete this.cb;");
2798        let _ = Reflect::set(&secondary_button, &"onClick".into(), &on_click);
2799        let _ = Reflect::set(&secondary_button, &"offClick".into(), &off_click);
2800
2801        let called = Rc::new(Cell::new(false));
2802        let called_clone = Rc::clone(&called);
2803
2804        let app = TelegramWebApp::instance().unwrap();
2805        let handle = app
2806            .set_bottom_button_callback(BottomButton::Secondary, move || {
2807                called_clone.set(true);
2808            })
2809            .unwrap();
2810
2811        let stored = Reflect::has(&secondary_button, &"cb".into()).unwrap();
2812        assert!(stored);
2813
2814        let cb_fn = Reflect::get(&secondary_button, &"cb".into())
2815            .unwrap()
2816            .dyn_into::<Function>()
2817            .unwrap();
2818        let _ = cb_fn.call0(&JsValue::NULL);
2819        assert!(called.get());
2820
2821        app.remove_bottom_button_callback(handle).unwrap();
2822        let stored_after = Reflect::has(&secondary_button, &"cb".into()).unwrap();
2823        assert!(!stored_after);
2824    }
2825
2826    #[wasm_bindgen_test]
2827    #[allow(dead_code, clippy::unused_unit)]
2828    fn on_event_register_and_remove() {
2829        let webapp = setup_webapp();
2830        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2831        let off_event = Function::new_with_args("name", "delete this[name];");
2832        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2833        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2834
2835        let app = TelegramWebApp::instance().unwrap();
2836        let handle = app.on_event("test", |_: JsValue| {}).unwrap();
2837        assert!(Reflect::has(&webapp, &"test".into()).unwrap());
2838        app.off_event(handle).unwrap();
2839        assert!(!Reflect::has(&webapp, &"test".into()).unwrap());
2840    }
2841
2842    #[wasm_bindgen_test]
2843    #[allow(dead_code, clippy::unused_unit)]
2844    fn background_event_register_and_remove() {
2845        let webapp = setup_webapp();
2846        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2847        let off_event = Function::new_with_args("name", "delete this[name];");
2848        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2849        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2850
2851        let app = TelegramWebApp::instance().unwrap();
2852        let handle = app
2853            .on_background_event(BackgroundEvent::MainButtonClicked, |_| {})
2854            .unwrap();
2855        assert!(Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
2856        app.off_event(handle).unwrap();
2857        assert!(!Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
2858    }
2859
2860    #[wasm_bindgen_test]
2861    #[allow(dead_code, clippy::unused_unit)]
2862    fn background_event_delivers_data() {
2863        let webapp = setup_webapp();
2864        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2865        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2866
2867        let app = TelegramWebApp::instance().unwrap();
2868        let received = Rc::new(RefCell::new(String::new()));
2869        let received_clone = Rc::clone(&received);
2870        let _handle = app
2871            .on_background_event(BackgroundEvent::InvoiceClosed, move |v| {
2872                *received_clone.borrow_mut() = v.as_string().unwrap_or_default();
2873            })
2874            .unwrap();
2875
2876        let cb = Reflect::get(&webapp, &"invoiceClosed".into())
2877            .unwrap()
2878            .dyn_into::<Function>()
2879            .unwrap();
2880        let _ = cb.call1(&JsValue::NULL, &JsValue::from_str("paid"));
2881        assert_eq!(received.borrow().as_str(), "paid");
2882    }
2883
2884    #[wasm_bindgen_test]
2885    #[allow(dead_code, clippy::unused_unit)]
2886    fn theme_changed_register_and_remove() {
2887        let webapp = setup_webapp();
2888        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2889        let off_event = Function::new_with_args("name", "delete this[name];");
2890        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2891        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2892
2893        let app = TelegramWebApp::instance().unwrap();
2894        let handle = app.on_theme_changed(|| {}).unwrap();
2895        assert!(Reflect::has(&webapp, &"themeChanged".into()).unwrap());
2896        app.off_event(handle).unwrap();
2897        assert!(!Reflect::has(&webapp, &"themeChanged".into()).unwrap());
2898    }
2899
2900    #[wasm_bindgen_test]
2901    #[allow(dead_code, clippy::unused_unit)]
2902    fn safe_area_changed_register_and_remove() {
2903        let webapp = setup_webapp();
2904        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2905        let off_event = Function::new_with_args("name", "delete this[name];");
2906        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2907        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2908
2909        let app = TelegramWebApp::instance().unwrap();
2910        let handle = app.on_safe_area_changed(|| {}).unwrap();
2911        assert!(Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
2912        app.off_event(handle).unwrap();
2913        assert!(!Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
2914    }
2915
2916    #[wasm_bindgen_test]
2917    #[allow(dead_code, clippy::unused_unit)]
2918    fn content_safe_area_changed_register_and_remove() {
2919        let webapp = setup_webapp();
2920        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2921        let off_event = Function::new_with_args("name", "delete this[name];");
2922        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2923        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2924
2925        let app = TelegramWebApp::instance().unwrap();
2926        let handle = app.on_content_safe_area_changed(|| {}).unwrap();
2927        assert!(Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
2928        app.off_event(handle).unwrap();
2929        assert!(!Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
2930    }
2931
2932    #[wasm_bindgen_test]
2933    #[allow(dead_code, clippy::unused_unit)]
2934    fn viewport_changed_register_and_remove() {
2935        let webapp = setup_webapp();
2936        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2937        let off_event = Function::new_with_args("name", "delete this[name];");
2938        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2939        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2940
2941        let app = TelegramWebApp::instance().unwrap();
2942        let handle = app.on_viewport_changed(|| {}).unwrap();
2943        assert!(Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
2944        app.off_event(handle).unwrap();
2945        assert!(!Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
2946    }
2947
2948    #[wasm_bindgen_test]
2949    #[allow(dead_code, clippy::unused_unit)]
2950    fn clipboard_text_received_register_and_remove() {
2951        let webapp = setup_webapp();
2952        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2953        let off_event = Function::new_with_args("name", "delete this[name];");
2954        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2955        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2956
2957        let app = TelegramWebApp::instance().unwrap();
2958        let handle = app.on_clipboard_text_received(|_| {}).unwrap();
2959        assert!(Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
2960        app.off_event(handle).unwrap();
2961        assert!(!Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
2962    }
2963
2964    #[wasm_bindgen_test]
2965    #[allow(dead_code, clippy::unused_unit)]
2966    fn open_link_and_telegram_link() {
2967        let webapp = setup_webapp();
2968        let open_link = Function::new_with_args("url", "this.open_link = url;");
2969        let open_tg_link = Function::new_with_args("url", "this.open_tg_link = url;");
2970        let _ = Reflect::set(&webapp, &"openLink".into(), &open_link);
2971        let _ = Reflect::set(&webapp, &"openTelegramLink".into(), &open_tg_link);
2972
2973        let app = TelegramWebApp::instance().unwrap();
2974        let url = "https://example.com";
2975        app.open_link(url, None).unwrap();
2976        app.open_telegram_link(url).unwrap();
2977
2978        assert_eq!(
2979            Reflect::get(&webapp, &"open_link".into())
2980                .unwrap()
2981                .as_string()
2982                .as_deref(),
2983            Some(url)
2984        );
2985        assert_eq!(
2986            Reflect::get(&webapp, &"open_tg_link".into())
2987                .unwrap()
2988                .as_string()
2989                .as_deref(),
2990            Some(url)
2991        );
2992    }
2993
2994    #[wasm_bindgen_test]
2995    #[allow(dead_code, clippy::unused_unit)]
2996    fn invoice_closed_register_and_remove() {
2997        let webapp = setup_webapp();
2998        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2999        let off_event = Function::new_with_args("name", "delete this[name];");
3000        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
3001        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
3002
3003        let app = TelegramWebApp::instance().unwrap();
3004        let handle = app.on_invoice_closed(|_| {}).unwrap();
3005        assert!(Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
3006        app.off_event(handle).unwrap();
3007        assert!(!Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
3008    }
3009
3010    #[wasm_bindgen_test]
3011    #[allow(dead_code, clippy::unused_unit)]
3012    fn invoice_closed_invokes_callback() {
3013        let webapp = setup_webapp();
3014        let on_event = Function::new_with_args("name, cb", "this.cb = cb;");
3015        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
3016
3017        let app = TelegramWebApp::instance().unwrap();
3018        let status = Rc::new(RefCell::new(String::new()));
3019        let status_clone = Rc::clone(&status);
3020        app.on_invoice_closed(move |s| {
3021            *status_clone.borrow_mut() = s;
3022        })
3023        .unwrap();
3024
3025        let cb = Reflect::get(&webapp, &"cb".into())
3026            .unwrap()
3027            .dyn_into::<Function>()
3028            .unwrap();
3029        cb.call1(&webapp, &"paid".into()).unwrap();
3030        assert_eq!(status.borrow().as_str(), "paid");
3031        cb.call1(&webapp, &"failed".into()).unwrap();
3032        assert_eq!(status.borrow().as_str(), "failed");
3033    }
3034
3035    #[wasm_bindgen_test]
3036    #[allow(dead_code, clippy::unused_unit)]
3037    fn open_invoice_invokes_callback() {
3038        let webapp = setup_webapp();
3039        let open_invoice = Function::new_with_args("url, cb", "cb('paid');");
3040        let _ = Reflect::set(&webapp, &"openInvoice".into(), &open_invoice);
3041
3042        let app = TelegramWebApp::instance().unwrap();
3043        let status = Rc::new(RefCell::new(String::new()));
3044        let status_clone = Rc::clone(&status);
3045
3046        app.open_invoice("https://invoice", move |s| {
3047            *status_clone.borrow_mut() = s;
3048        })
3049        .unwrap();
3050
3051        assert_eq!(status.borrow().as_str(), "paid");
3052    }
3053
3054    #[wasm_bindgen_test]
3055    #[allow(dead_code, clippy::unused_unit)]
3056    fn switch_inline_query_calls_js() {
3057        let webapp = setup_webapp();
3058        let switch_inline =
3059            Function::new_with_args("query, types", "this.query = query; this.types = types;");
3060        let _ = Reflect::set(&webapp, &"switchInlineQuery".into(), &switch_inline);
3061
3062        let app = TelegramWebApp::instance().unwrap();
3063        let types = JsValue::from_str("users");
3064        app.switch_inline_query("search", Some(&types)).unwrap();
3065
3066        assert_eq!(
3067            Reflect::get(&webapp, &"query".into())
3068                .unwrap()
3069                .as_string()
3070                .as_deref(),
3071            Some("search"),
3072        );
3073        assert_eq!(
3074            Reflect::get(&webapp, &"types".into())
3075                .unwrap()
3076                .as_string()
3077                .as_deref(),
3078            Some("users"),
3079        );
3080    }
3081
3082    #[wasm_bindgen_test]
3083    #[allow(dead_code, clippy::unused_unit)]
3084    fn share_message_calls_js() {
3085        let webapp = setup_webapp();
3086        let share = Function::new_with_args("id, cb", "this.shared_id = id; cb(true);");
3087        let _ = Reflect::set(&webapp, &"shareMessage".into(), &share);
3088
3089        let app = TelegramWebApp::instance().unwrap();
3090        let sent = Rc::new(Cell::new(false));
3091        let sent_clone = Rc::clone(&sent);
3092
3093        app.share_message("123", move |s| {
3094            sent_clone.set(s);
3095        })
3096        .unwrap();
3097
3098        assert_eq!(
3099            Reflect::get(&webapp, &"shared_id".into())
3100                .unwrap()
3101                .as_string()
3102                .as_deref(),
3103            Some("123"),
3104        );
3105        assert!(sent.get());
3106    }
3107
3108    #[wasm_bindgen_test]
3109    #[allow(dead_code, clippy::unused_unit)]
3110    fn share_to_story_calls_js() {
3111        let webapp = setup_webapp();
3112        let share = Function::new_with_args(
3113            "url, params",
3114            "this.story_url = url; this.story_params = params;"
3115        );
3116        let _ = Reflect::set(&webapp, &"shareToStory".into(), &share);
3117
3118        let app = TelegramWebApp::instance().unwrap();
3119        let url = "https://example.com/media";
3120        let params = Object::new();
3121        let _ = Reflect::set(&params, &"text".into(), &"hi".into());
3122        app.share_to_story(url, Some(&params.into())).unwrap();
3123
3124        assert_eq!(
3125            Reflect::get(&webapp, &"story_url".into())
3126                .unwrap()
3127                .as_string()
3128                .as_deref(),
3129            Some(url),
3130        );
3131        let stored = Reflect::get(&webapp, &"story_params".into()).unwrap();
3132        assert_eq!(
3133            Reflect::get(&stored, &"text".into())
3134                .unwrap()
3135                .as_string()
3136                .as_deref(),
3137            Some("hi"),
3138        );
3139    }
3140
3141    #[wasm_bindgen_test]
3142    #[allow(dead_code, clippy::unused_unit)]
3143    fn share_url_calls_js() {
3144        let webapp = setup_webapp();
3145        let share = Function::new_with_args(
3146            "url, text",
3147            "this.shared_url = url; this.shared_text = text;"
3148        );
3149        let _ = Reflect::set(&webapp, &"shareURL".into(), &share);
3150
3151        let app = TelegramWebApp::instance().unwrap();
3152        let url = "https://example.com";
3153        let text = "check";
3154        app.share_url(url, Some(text)).unwrap();
3155
3156        assert_eq!(
3157            Reflect::get(&webapp, &"shared_url".into())
3158                .unwrap()
3159                .as_string()
3160                .as_deref(),
3161            Some(url),
3162        );
3163        assert_eq!(
3164            Reflect::get(&webapp, &"shared_text".into())
3165                .unwrap()
3166                .as_string()
3167                .as_deref(),
3168            Some(text),
3169        );
3170    }
3171
3172    #[wasm_bindgen_test]
3173    #[allow(dead_code, clippy::unused_unit)]
3174    fn join_voice_chat_calls_js() {
3175        let webapp = setup_webapp();
3176        let join = Function::new_with_args(
3177            "id, hash",
3178            "this.voice_chat_id = id; this.voice_chat_hash = hash;"
3179        );
3180        let _ = Reflect::set(&webapp, &"joinVoiceChat".into(), &join);
3181
3182        let app = TelegramWebApp::instance().unwrap();
3183        app.join_voice_chat("123", Some("hash")).unwrap();
3184
3185        assert_eq!(
3186            Reflect::get(&webapp, &"voice_chat_id".into())
3187                .unwrap()
3188                .as_string()
3189                .as_deref(),
3190            Some("123"),
3191        );
3192        assert_eq!(
3193            Reflect::get(&webapp, &"voice_chat_hash".into())
3194                .unwrap()
3195                .as_string()
3196                .as_deref(),
3197            Some("hash"),
3198        );
3199    }
3200
3201    #[wasm_bindgen_test]
3202    #[allow(dead_code, clippy::unused_unit)]
3203    fn add_to_home_screen_calls_js() {
3204        let webapp = setup_webapp();
3205        let add = Function::new_with_args("", "this.called = true; return true;");
3206        let _ = Reflect::set(&webapp, &"addToHomeScreen".into(), &add);
3207
3208        let app = TelegramWebApp::instance().unwrap();
3209        let shown = app.add_to_home_screen().unwrap();
3210        assert!(shown);
3211        let called = Reflect::get(&webapp, &"called".into())
3212            .unwrap()
3213            .as_bool()
3214            .unwrap_or(false);
3215        assert!(called);
3216    }
3217
3218    #[wasm_bindgen_test]
3219    #[allow(dead_code, clippy::unused_unit)]
3220    fn request_fullscreen_calls_js() {
3221        let webapp = setup_webapp();
3222        let called = Rc::new(Cell::new(false));
3223        let called_clone = Rc::clone(&called);
3224
3225        let cb = Closure::<dyn FnMut()>::new(move || {
3226            called_clone.set(true);
3227        });
3228        let _ = Reflect::set(
3229            &webapp,
3230            &"requestFullscreen".into(),
3231            cb.as_ref().unchecked_ref()
3232        );
3233        cb.forget();
3234
3235        let app = TelegramWebApp::instance().unwrap();
3236        app.request_fullscreen().unwrap();
3237        assert!(called.get());
3238    }
3239
3240    #[wasm_bindgen_test]
3241    #[allow(dead_code, clippy::unused_unit)]
3242    fn exit_fullscreen_calls_js() {
3243        let webapp = setup_webapp();
3244        let called = Rc::new(Cell::new(false));
3245        let called_clone = Rc::clone(&called);
3246
3247        let cb = Closure::<dyn FnMut()>::new(move || {
3248            called_clone.set(true);
3249        });
3250        let _ = Reflect::set(
3251            &webapp,
3252            &"exitFullscreen".into(),
3253            cb.as_ref().unchecked_ref()
3254        );
3255        cb.forget();
3256
3257        let app = TelegramWebApp::instance().unwrap();
3258        app.exit_fullscreen().unwrap();
3259        assert!(called.get());
3260    }
3261
3262    #[wasm_bindgen_test]
3263    #[allow(dead_code, clippy::unused_unit)]
3264    fn check_home_screen_status_invokes_callback() {
3265        let webapp = setup_webapp();
3266        let check = Function::new_with_args("cb", "cb('added');");
3267        let _ = Reflect::set(&webapp, &"checkHomeScreenStatus".into(), &check);
3268
3269        let app = TelegramWebApp::instance().unwrap();
3270        let status = Rc::new(RefCell::new(String::new()));
3271        let status_clone = Rc::clone(&status);
3272
3273        app.check_home_screen_status(move |s| {
3274            *status_clone.borrow_mut() = s;
3275        })
3276        .unwrap();
3277
3278        assert_eq!(status.borrow().as_str(), "added");
3279    }
3280
3281    #[wasm_bindgen_test]
3282    #[allow(dead_code, clippy::unused_unit)]
3283    fn lock_orientation_calls_js() {
3284        let webapp = setup_webapp();
3285        let received = Rc::new(RefCell::new(None));
3286        let rc_clone = Rc::clone(&received);
3287
3288        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
3289            *rc_clone.borrow_mut() = v.as_string();
3290        });
3291        let _ = Reflect::set(
3292            &webapp,
3293            &"lockOrientation".into(),
3294            cb.as_ref().unchecked_ref()
3295        );
3296        cb.forget();
3297
3298        let app = TelegramWebApp::instance().unwrap();
3299        app.lock_orientation("portrait").unwrap();
3300        assert_eq!(received.borrow().as_deref(), Some("portrait"));
3301    }
3302
3303    #[wasm_bindgen_test]
3304    #[allow(dead_code, clippy::unused_unit)]
3305    fn unlock_orientation_calls_js() {
3306        let webapp = setup_webapp();
3307        let called = Rc::new(Cell::new(false));
3308        let called_clone = Rc::clone(&called);
3309
3310        let cb = Closure::<dyn FnMut()>::new(move || {
3311            called_clone.set(true);
3312        });
3313        let _ = Reflect::set(
3314            &webapp,
3315            &"unlockOrientation".into(),
3316            cb.as_ref().unchecked_ref()
3317        );
3318        cb.forget();
3319
3320        let app = TelegramWebApp::instance().unwrap();
3321        app.unlock_orientation().unwrap();
3322        assert!(called.get());
3323    }
3324
3325    #[wasm_bindgen_test]
3326    #[allow(dead_code, clippy::unused_unit)]
3327    fn enable_vertical_swipes_calls_js() {
3328        let webapp = setup_webapp();
3329        let called = Rc::new(Cell::new(false));
3330        let called_clone = Rc::clone(&called);
3331
3332        let cb = Closure::<dyn FnMut()>::new(move || {
3333            called_clone.set(true);
3334        });
3335        let _ = Reflect::set(
3336            &webapp,
3337            &"enableVerticalSwipes".into(),
3338            cb.as_ref().unchecked_ref()
3339        );
3340        cb.forget();
3341
3342        let app = TelegramWebApp::instance().unwrap();
3343        app.enable_vertical_swipes().unwrap();
3344        assert!(called.get());
3345    }
3346
3347    #[wasm_bindgen_test]
3348    #[allow(dead_code, clippy::unused_unit)]
3349    fn disable_vertical_swipes_calls_js() {
3350        let webapp = setup_webapp();
3351        let called = Rc::new(Cell::new(false));
3352        let called_clone = Rc::clone(&called);
3353
3354        let cb = Closure::<dyn FnMut()>::new(move || {
3355            called_clone.set(true);
3356        });
3357        let _ = Reflect::set(
3358            &webapp,
3359            &"disableVerticalSwipes".into(),
3360            cb.as_ref().unchecked_ref()
3361        );
3362        cb.forget();
3363
3364        let app = TelegramWebApp::instance().unwrap();
3365        app.disable_vertical_swipes().unwrap();
3366        assert!(called.get());
3367    }
3368
3369    #[wasm_bindgen_test]
3370    #[allow(dead_code, clippy::unused_unit)]
3371    fn request_write_access_invokes_callback() {
3372        let webapp = setup_webapp();
3373        let request = Function::new_with_args("cb", "cb(true);");
3374        let _ = Reflect::set(&webapp, &"requestWriteAccess".into(), &request);
3375
3376        let app = TelegramWebApp::instance().unwrap();
3377        let granted = Rc::new(Cell::new(false));
3378        let granted_clone = Rc::clone(&granted);
3379
3380        let res = app.request_write_access(move |g| {
3381            granted_clone.set(g);
3382        });
3383        assert!(res.is_ok());
3384
3385        assert!(granted.get());
3386    }
3387
3388    #[wasm_bindgen_test]
3389    #[allow(dead_code, clippy::unused_unit)]
3390    fn download_file_invokes_callback() {
3391        let webapp = setup_webapp();
3392        let received_url = Rc::new(RefCell::new(String::new()));
3393        let received_name = Rc::new(RefCell::new(String::new()));
3394        let url_clone = Rc::clone(&received_url);
3395        let name_clone = Rc::clone(&received_name);
3396
3397        let download = Closure::<dyn FnMut(JsValue, JsValue)>::new(move |params, cb: JsValue| {
3398            let url = Reflect::get(&params, &"url".into())
3399                .unwrap()
3400                .as_string()
3401                .unwrap_or_default();
3402            let name = Reflect::get(&params, &"file_name".into())
3403                .unwrap()
3404                .as_string()
3405                .unwrap_or_default();
3406            *url_clone.borrow_mut() = url;
3407            *name_clone.borrow_mut() = name;
3408            let func = cb.dyn_ref::<Function>().unwrap();
3409            let _ = func.call1(&JsValue::NULL, &JsValue::from_str("id"));
3410        });
3411        let _ = Reflect::set(
3412            &webapp,
3413            &"downloadFile".into(),
3414            download.as_ref().unchecked_ref()
3415        );
3416        download.forget();
3417
3418        let app = TelegramWebApp::instance().unwrap();
3419        let result = Rc::new(RefCell::new(String::new()));
3420        let result_clone = Rc::clone(&result);
3421        let params = DownloadFileParams {
3422            url:       "https://example.com/data.bin",
3423            file_name: Some("data.bin"),
3424            mime_type: None
3425        };
3426        app.download_file(params, move |id| {
3427            *result_clone.borrow_mut() = id;
3428        })
3429        .unwrap();
3430
3431        assert_eq!(
3432            received_url.borrow().as_str(),
3433            "https://example.com/data.bin"
3434        );
3435        assert_eq!(received_name.borrow().as_str(), "data.bin");
3436        assert_eq!(result.borrow().as_str(), "id");
3437    }
3438
3439    #[wasm_bindgen_test]
3440    #[allow(dead_code, clippy::unused_unit)]
3441    fn request_write_access_returns_error_when_missing() {
3442        let _webapp = setup_webapp();
3443        let app = TelegramWebApp::instance().unwrap();
3444        let res = app.request_write_access(|_| {});
3445        assert!(res.is_err());
3446    }
3447    #[wasm_bindgen_test]
3448    #[allow(dead_code, clippy::unused_unit)]
3449    fn request_emoji_status_access_invokes_callback() {
3450        let webapp = setup_webapp();
3451        let request = Function::new_with_args("cb", "cb(false);");
3452        let _ = Reflect::set(&webapp, &"requestEmojiStatusAccess".into(), &request);
3453
3454        let app = TelegramWebApp::instance().unwrap();
3455        let granted = Rc::new(Cell::new(true));
3456        let granted_clone = Rc::clone(&granted);
3457
3458        app.request_emoji_status_access(move |g| {
3459            granted_clone.set(g);
3460        })
3461        .unwrap();
3462
3463        assert!(!granted.get());
3464    }
3465
3466    #[wasm_bindgen_test]
3467    #[allow(dead_code, clippy::unused_unit)]
3468    fn set_emoji_status_invokes_callback() {
3469        let webapp = setup_webapp();
3470        let set_status = Function::new_with_args("status, cb", "this.st = status; cb(true);");
3471        let _ = Reflect::set(&webapp, &"setEmojiStatus".into(), &set_status);
3472
3473        let status = Object::new();
3474        let _ = Reflect::set(
3475            &status,
3476            &"custom_emoji_id".into(),
3477            &JsValue::from_str("321")
3478        );
3479
3480        let app = TelegramWebApp::instance().unwrap();
3481        let success = Rc::new(Cell::new(false));
3482        let success_clone = Rc::clone(&success);
3483
3484        app.set_emoji_status(&status.into(), move |s| {
3485            success_clone.set(s);
3486        })
3487        .unwrap();
3488
3489        assert!(success.get());
3490        let stored = Reflect::get(&webapp, &"st".into()).unwrap();
3491        let id = Reflect::get(&stored, &"custom_emoji_id".into())
3492            .unwrap()
3493            .as_string();
3494        assert_eq!(id.as_deref(), Some("321"));
3495    }
3496
3497    #[wasm_bindgen_test]
3498    #[allow(dead_code, clippy::unused_unit)]
3499    fn show_popup_invokes_callback() {
3500        let webapp = setup_webapp();
3501        let show_popup = Function::new_with_args("params, cb", "cb('ok');");
3502        let _ = Reflect::set(&webapp, &"showPopup".into(), &show_popup);
3503
3504        let app = TelegramWebApp::instance().unwrap();
3505        let button = Rc::new(RefCell::new(String::new()));
3506        let button_clone = Rc::clone(&button);
3507
3508        app.show_popup(&JsValue::NULL, move |id| {
3509            *button_clone.borrow_mut() = id;
3510        })
3511        .unwrap();
3512
3513        assert_eq!(button.borrow().as_str(), "ok");
3514    }
3515
3516    #[wasm_bindgen_test]
3517    #[allow(dead_code, clippy::unused_unit)]
3518    fn read_text_from_clipboard_invokes_callback() {
3519        let webapp = setup_webapp();
3520        let read_clip = Function::new_with_args("cb", "cb('clip');");
3521        let _ = Reflect::set(&webapp, &"readTextFromClipboard".into(), &read_clip);
3522
3523        let app = TelegramWebApp::instance().unwrap();
3524        let text = Rc::new(RefCell::new(String::new()));
3525        let text_clone = Rc::clone(&text);
3526
3527        app.read_text_from_clipboard(move |t| {
3528            *text_clone.borrow_mut() = t;
3529        })
3530        .unwrap();
3531
3532        assert_eq!(text.borrow().as_str(), "clip");
3533    }
3534
3535    #[wasm_bindgen_test]
3536    #[allow(dead_code, clippy::unused_unit)]
3537    fn scan_qr_popup_invokes_callback_and_close() {
3538        let webapp = setup_webapp();
3539        let show_scan = Function::new_with_args("text, cb", "cb('code');");
3540        let close_scan = Function::new_with_args("", "this.closed = true;");
3541        let _ = Reflect::set(&webapp, &"showScanQrPopup".into(), &show_scan);
3542        let _ = Reflect::set(&webapp, &"closeScanQrPopup".into(), &close_scan);
3543
3544        let app = TelegramWebApp::instance().unwrap();
3545        let text = Rc::new(RefCell::new(String::new()));
3546        let text_clone = Rc::clone(&text);
3547
3548        app.show_scan_qr_popup("scan", move |value| {
3549            *text_clone.borrow_mut() = value;
3550        })
3551        .unwrap();
3552        assert_eq!(text.borrow().as_str(), "code");
3553
3554        app.close_scan_qr_popup().unwrap();
3555        let closed = Reflect::get(&webapp, &"closed".into())
3556            .unwrap()
3557            .as_bool()
3558            .unwrap_or(false);
3559        assert!(closed);
3560    }
3561}