telegram_webapp_sdk/
webapp.rs

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