Skip to main content

telegram_webapp_sdk/webapp/
buttons.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_wasm_bindgen::to_value;
6use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
7
8use crate::{
9    logger,
10    webapp::{
11        TelegramWebApp,
12        types::{
13            BottomButton, BottomButtonParams, EventHandle, SecondaryButtonParams,
14            SecondaryButtonPosition
15        }
16    }
17};
18
19impl TelegramWebApp {
20    // === Internal bottom button helpers ===
21
22    pub(super) fn bottom_button_object(&self, button: BottomButton) -> Result<Object, JsValue> {
23        let name = button.js_name();
24        Reflect::get(&self.inner, &name.into())
25            .inspect_err(|_| logger::error(&format!("{name} not available")))?
26            .dyn_into::<Object>()
27            .inspect_err(|_| logger::error(&format!("{name} is not an object")))
28    }
29
30    pub(super) fn bottom_button_method(
31        &self,
32        button: BottomButton,
33        method: &str,
34        arg: Option<&JsValue>
35    ) -> Result<(), JsValue> {
36        let name = button.js_name();
37        let btn = self.bottom_button_object(button)?;
38        let f = Reflect::get(&btn, &method.into())
39            .inspect_err(|_| logger::error(&format!("{name}.{method} not available")))?;
40        let func = f.dyn_ref::<Function>().ok_or_else(|| {
41            logger::error(&format!("{name}.{method} is not a function"));
42            JsValue::from_str("not a function")
43        })?;
44        let result = match arg {
45            Some(v) => func.call1(&btn, v),
46            None => func.call0(&btn)
47        };
48        result.inspect_err(|_| logger::error(&format!("{name}.{method} call failed")))?;
49        Ok(())
50    }
51
52    pub(super) fn bottom_button_property(
53        &self,
54        button: BottomButton,
55        property: &str
56    ) -> Option<JsValue> {
57        self.bottom_button_object(button)
58            .ok()
59            .and_then(|object| Reflect::get(&object, &property.into()).ok())
60    }
61
62    // === Bottom button operations ===
63
64    /// Call `WebApp.MainButton.show()` or `WebApp.SecondaryButton.show()`.
65    ///
66    /// # Errors
67    /// Returns [`JsValue`] if the underlying JS call fails.
68    pub fn show_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
69        self.bottom_button_method(button, "show", None)
70    }
71
72    /// Hide a bottom button.
73    ///
74    /// # Errors
75    /// Returns [`JsValue`] if the underlying JS call fails.
76    pub fn hide_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
77        self.bottom_button_method(button, "hide", None)
78    }
79
80    /// Set bottom button text.
81    ///
82    /// # Errors
83    /// Returns [`JsValue`] if the underlying JS call fails.
84    pub fn set_bottom_button_text(&self, button: BottomButton, text: &str) -> Result<(), JsValue> {
85        self.bottom_button_method(button, "setText", Some(&text.into()))
86    }
87
88    /// Set bottom button color (`setColor(color)`).
89    ///
90    /// # Errors
91    /// Returns [`JsValue`] if the underlying JS call fails.
92    ///
93    /// # Examples
94    /// ```no_run
95    /// # use telegram_webapp_sdk::webapp::{TelegramWebApp, BottomButton};
96    /// # let app = TelegramWebApp::instance().unwrap();
97    /// let _ = app.set_bottom_button_color(BottomButton::Main, "#ff0000");
98    /// ```
99    pub fn set_bottom_button_color(
100        &self,
101        button: BottomButton,
102        color: &str
103    ) -> Result<(), JsValue> {
104        self.bottom_button_method(button, "setColor", Some(&color.into()))
105    }
106
107    /// Set bottom button text color (`setTextColor(color)`).
108    ///
109    /// # Errors
110    /// Returns [`JsValue`] if the underlying JS call fails.
111    ///
112    /// # Examples
113    /// ```no_run
114    /// # use telegram_webapp_sdk::webapp::{TelegramWebApp, BottomButton};
115    /// # let app = TelegramWebApp::instance().unwrap();
116    /// let _ = app.set_bottom_button_text_color(BottomButton::Main, "#ffffff");
117    /// ```
118    pub fn set_bottom_button_text_color(
119        &self,
120        button: BottomButton,
121        color: &str
122    ) -> Result<(), JsValue> {
123        self.bottom_button_method(button, "setTextColor", Some(&color.into()))
124    }
125
126    /// Set custom emoji icon for a bottom button (Bot API 9.5+).
127    ///
128    /// Sets the custom emoji ID to be displayed as an icon on the button.
129    /// Use an empty string to remove the icon.
130    ///
131    /// # Arguments
132    /// * `button` - The button to update (Main or Secondary)
133    /// * `icon_id` - The custom emoji ID (e.g., "123456789")
134    ///
135    /// # Errors
136    /// Returns [`JsValue`] if the underlying JS call fails.
137    ///
138    /// # Examples
139    /// ```no_run
140    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
141    ///
142    /// if let Some(app) = TelegramWebApp::instance() {
143    ///     let _ = app.set_bottom_button_icon_custom_emoji_id(BottomButton::Main, "123456789");
144    /// }
145    /// ```
146    pub fn set_bottom_button_icon_custom_emoji_id(
147        &self,
148        button: BottomButton,
149        icon_id: &str
150    ) -> Result<(), JsValue> {
151        self.bottom_button_method(button, "setIconCustomEmojiId", Some(&icon_id.into()))
152    }
153
154    /// Enable a bottom button, allowing user interaction.
155    ///
156    /// # Examples
157    /// ```no_run
158    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
159    ///
160    /// if let Some(app) = TelegramWebApp::instance() {
161    ///     let _ = app.enable_bottom_button(BottomButton::Main);
162    /// }
163    /// ```
164    pub fn enable_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
165        self.bottom_button_method(button, "enable", None)
166    }
167
168    /// Disable a bottom button, preventing user interaction.
169    ///
170    /// # Examples
171    /// ```no_run
172    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
173    ///
174    /// if let Some(app) = TelegramWebApp::instance() {
175    ///     let _ = app.disable_bottom_button(BottomButton::Main);
176    /// }
177    /// ```
178    pub fn disable_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
179        self.bottom_button_method(button, "disable", None)
180    }
181
182    /// Show the circular loading indicator on a bottom button.
183    ///
184    /// # Examples
185    /// ```no_run
186    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
187    ///
188    /// if let Some(app) = TelegramWebApp::instance() {
189    ///     let _ = app.show_bottom_button_progress(BottomButton::Main, false);
190    /// }
191    /// ```
192    pub fn show_bottom_button_progress(
193        &self,
194        button: BottomButton,
195        leave_active: bool
196    ) -> Result<(), JsValue> {
197        let leave_active = JsValue::from_bool(leave_active);
198        self.bottom_button_method(button, "showProgress", Some(&leave_active))
199    }
200
201    /// Hide the loading indicator on a bottom button.
202    ///
203    /// # Examples
204    /// ```no_run
205    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
206    ///
207    /// if let Some(app) = TelegramWebApp::instance() {
208    ///     let _ = app.hide_bottom_button_progress(BottomButton::Main);
209    /// }
210    /// ```
211    pub fn hide_bottom_button_progress(&self, button: BottomButton) -> Result<(), JsValue> {
212        self.bottom_button_method(button, "hideProgress", None)
213    }
214
215    /// Returns whether the specified bottom button is currently visible.
216    ///
217    /// # Examples
218    /// ```no_run
219    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
220    ///
221    /// if let Some(app) = TelegramWebApp::instance() {
222    ///     let _ = app.is_bottom_button_visible(BottomButton::Main);
223    /// }
224    /// ```
225    pub fn is_bottom_button_visible(&self, button: BottomButton) -> bool {
226        self.bottom_button_property(button, "isVisible")
227            .and_then(|v| v.as_bool())
228            .unwrap_or(false)
229    }
230
231    /// Returns whether the specified bottom button is active (enabled).
232    ///
233    /// # Examples
234    /// ```no_run
235    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
236    ///
237    /// if let Some(app) = TelegramWebApp::instance() {
238    ///     let _ = app.is_bottom_button_active(BottomButton::Main);
239    /// }
240    /// ```
241    pub fn is_bottom_button_active(&self, button: BottomButton) -> bool {
242        self.bottom_button_property(button, "isActive")
243            .and_then(|v| v.as_bool())
244            .unwrap_or(false)
245    }
246
247    /// Returns whether the progress indicator is visible on the button.
248    ///
249    /// # Examples
250    /// ```no_run
251    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
252    ///
253    /// if let Some(app) = TelegramWebApp::instance() {
254    ///     let _ = app.is_bottom_button_progress_visible(BottomButton::Main);
255    /// }
256    /// ```
257    pub fn is_bottom_button_progress_visible(&self, button: BottomButton) -> bool {
258        self.bottom_button_property(button, "isProgressVisible")
259            .and_then(|v| v.as_bool())
260            .unwrap_or(false)
261    }
262
263    /// Returns the current text displayed on the button.
264    ///
265    /// # Examples
266    /// ```no_run
267    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
268    ///
269    /// if let Some(app) = TelegramWebApp::instance() {
270    ///     let _ = app.bottom_button_text(BottomButton::Main);
271    /// }
272    /// ```
273    pub fn bottom_button_text(&self, button: BottomButton) -> Option<String> {
274        self.bottom_button_property(button, "text")?.as_string()
275    }
276
277    /// Returns the current custom emoji icon ID of the button (Bot API 9.5+).
278    ///
279    /// # Arguments
280    /// * `button` - The button to query (Main or Secondary)
281    ///
282    /// # Examples
283    /// ```no_run
284    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
285    ///
286    /// if let Some(app) = TelegramWebApp::instance() {
287    ///     if let Some(icon_id) = app.bottom_button_icon_custom_emoji_id(BottomButton::Main) {
288    ///         println!("Icon ID: {}", icon_id);
289    ///     }
290    /// }
291    /// ```
292    pub fn bottom_button_icon_custom_emoji_id(&self, button: BottomButton) -> Option<String> {
293        self.bottom_button_property(button, "iconCustomEmojiId")?
294            .as_string()
295    }
296
297    /// Returns the current text color of the button.
298    ///
299    /// # Examples
300    /// ```no_run
301    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
302    ///
303    /// if let Some(app) = TelegramWebApp::instance() {
304    ///     let _ = app.bottom_button_text_color(BottomButton::Main);
305    /// }
306    /// ```
307    pub fn bottom_button_text_color(&self, button: BottomButton) -> Option<String> {
308        self.bottom_button_property(button, "textColor")?
309            .as_string()
310    }
311
312    /// Returns the current background color of the button.
313    ///
314    /// # Examples
315    /// ```no_run
316    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
317    ///
318    /// if let Some(app) = TelegramWebApp::instance() {
319    ///     let _ = app.bottom_button_color(BottomButton::Main);
320    /// }
321    /// ```
322    pub fn bottom_button_color(&self, button: BottomButton) -> Option<String> {
323        self.bottom_button_property(button, "color")?.as_string()
324    }
325
326    /// Returns whether the shine effect is enabled on the button.
327    ///
328    /// # Examples
329    /// ```no_run
330    /// use telegram_webapp_sdk::webapp::{BottomButton, TelegramWebApp};
331    ///
332    /// if let Some(app) = TelegramWebApp::instance() {
333    ///     let _ = app.bottom_button_has_shine_effect(BottomButton::Main);
334    /// }
335    /// ```
336    pub fn bottom_button_has_shine_effect(&self, button: BottomButton) -> bool {
337        self.bottom_button_property(button, "hasShineEffect")
338            .and_then(|v| v.as_bool())
339            .unwrap_or(false)
340    }
341
342    /// Update bottom button state via `setParams`.
343    ///
344    /// # Examples
345    /// ```no_run
346    /// use telegram_webapp_sdk::webapp::{BottomButton, BottomButtonParams, TelegramWebApp};
347    ///
348    /// if let Some(app) = TelegramWebApp::instance() {
349    ///     let params = BottomButtonParams {
350    ///         text: Some("Send"),
351    ///         ..Default::default()
352    ///     };
353    ///     let _ = app.set_bottom_button_params(BottomButton::Main, &params);
354    /// }
355    /// ```
356    pub fn set_bottom_button_params(
357        &self,
358        button: BottomButton,
359        params: &BottomButtonParams<'_>
360    ) -> Result<(), JsValue> {
361        let value = to_value(params).map_err(|err| JsValue::from_str(&err.to_string()))?;
362        self.bottom_button_method(button, "setParams", Some(&value))
363    }
364
365    /// Update secondary button state via `setParams`, including position.
366    ///
367    /// # Examples
368    /// ```no_run
369    /// use telegram_webapp_sdk::webapp::{
370    ///     SecondaryButtonParams, SecondaryButtonPosition, TelegramWebApp
371    /// };
372    ///
373    /// if let Some(app) = TelegramWebApp::instance() {
374    ///     let params = SecondaryButtonParams {
375    ///         position: Some(SecondaryButtonPosition::Left),
376    ///         ..Default::default()
377    ///     };
378    ///     let _ = app.set_secondary_button_params(&params);
379    /// }
380    /// ```
381    pub fn set_secondary_button_params(
382        &self,
383        params: &SecondaryButtonParams<'_>
384    ) -> Result<(), JsValue> {
385        let value = to_value(params).map_err(|err| JsValue::from_str(&err.to_string()))?;
386        self.bottom_button_method(BottomButton::Secondary, "setParams", Some(&value))
387    }
388
389    /// Returns the configured position of the secondary button, if available.
390    ///
391    /// # Examples
392    /// ```no_run
393    /// use telegram_webapp_sdk::webapp::{SecondaryButtonPosition, TelegramWebApp};
394    ///
395    /// if let Some(app) = TelegramWebApp::instance() {
396    ///     let _ = app.secondary_button_position();
397    /// }
398    /// ```
399    pub fn secondary_button_position(&self) -> Option<SecondaryButtonPosition> {
400        self.bottom_button_property(BottomButton::Secondary, "position")
401            .and_then(SecondaryButtonPosition::from_js_value)
402    }
403
404    /// Set callback for `onClick()` on a bottom button.
405    ///
406    /// Returns an [`EventHandle`] that can be used to remove the callback.
407    ///
408    /// # Errors
409    /// Returns [`JsValue`] if the underlying JS call fails.
410    pub fn set_bottom_button_callback<F>(
411        &self,
412        button: BottomButton,
413        callback: F
414    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
415    where
416        F: 'static + Fn()
417    {
418        let btn_val = Reflect::get(&self.inner, &button.js_name().into())?;
419        let btn = btn_val.dyn_into::<Object>()?;
420        let cb = Closure::<dyn FnMut()>::new(callback);
421        let f = Reflect::get(&btn, &"onClick".into())?;
422        let func = f
423            .dyn_ref::<Function>()
424            .ok_or_else(|| JsValue::from_str("onClick is not a function"))?;
425        func.call1(&btn, cb.as_ref().unchecked_ref())?;
426        Ok(EventHandle::new(btn, "offClick", None, cb))
427    }
428
429    /// Remove previously set bottom button callback.
430    ///
431    /// # Errors
432    /// Returns [`JsValue`] if the underlying JS call fails.
433    pub fn remove_bottom_button_callback(
434        &self,
435        handle: EventHandle<dyn FnMut()>
436    ) -> Result<(), JsValue> {
437        handle.unregister()
438    }
439
440    // === Back button operations ===
441
442    /// Show back button.
443    ///
444    /// # Errors
445    /// Returns [`JsValue`] if the underlying JS call fails.
446    pub fn show_back_button(&self) -> Result<(), JsValue> {
447        self.call_nested0("BackButton", "show")
448    }
449
450    /// Hide back button.
451    ///
452    /// # Errors
453    /// Returns [`JsValue`] if the underlying JS call fails.
454    pub fn hide_back_button(&self) -> Result<(), JsValue> {
455        self.call_nested0("BackButton", "hide")
456    }
457
458    /// Registers a callback for the native back button.
459    ///
460    /// Returns an [`EventHandle`] that can be passed to
461    /// [`remove_back_button_callback`](Self::remove_back_button_callback).
462    ///
463    /// # Examples
464    /// ```no_run
465    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
466    /// # let app = TelegramWebApp::instance().unwrap();
467    /// let handle = app.set_back_button_callback(|| {}).expect("callback");
468    /// app.remove_back_button_callback(handle).unwrap();
469    /// ```
470    ///
471    /// # Errors
472    /// Returns [`JsValue`] if the underlying JS call fails.
473    pub fn set_back_button_callback<F>(
474        &self,
475        callback: F
476    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
477    where
478        F: 'static + Fn()
479    {
480        let back_button_val = Reflect::get(&self.inner, &"BackButton".into())?;
481        let back_button = back_button_val.dyn_into::<Object>()?;
482        let cb = Closure::<dyn FnMut()>::new(callback);
483        let f = Reflect::get(&back_button, &"onClick".into())?;
484        let func = f
485            .dyn_ref::<Function>()
486            .ok_or_else(|| JsValue::from_str("onClick is not a function"))?;
487        func.call1(&back_button, cb.as_ref().unchecked_ref())?;
488        Ok(EventHandle::new(back_button, "offClick", None, cb))
489    }
490
491    /// Remove previously set back button callback.
492    ///
493    /// # Errors
494    /// Returns [`JsValue`] if the underlying JS call fails.
495    pub fn remove_back_button_callback(
496        &self,
497        handle: EventHandle<dyn FnMut()>
498    ) -> Result<(), JsValue> {
499        handle.unregister()
500    }
501
502    /// Returns whether the native back button is visible.
503    ///
504    /// # Examples
505    /// ```no_run
506    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
507    /// # let app = TelegramWebApp::instance().unwrap();
508    /// let _ = app.is_back_button_visible();
509    /// ```
510    pub fn is_back_button_visible(&self) -> bool {
511        Reflect::get(&self.inner, &"BackButton".into())
512            .ok()
513            .and_then(|bb| Reflect::get(&bb, &"isVisible".into()).ok())
514            .and_then(|v| v.as_bool())
515            .unwrap_or(false)
516    }
517
518    // === Legacy aliases for main button ===
519
520    /// Legacy alias for [`Self::show_bottom_button`] with
521    /// [`BottomButton::Main`].
522    pub fn show_main_button(&self) -> Result<(), JsValue> {
523        self.show_bottom_button(BottomButton::Main)
524    }
525
526    /// Legacy alias for [`Self::hide_bottom_button`] with
527    /// [`BottomButton::Main`].
528    pub fn hide_main_button(&self) -> Result<(), JsValue> {
529        self.hide_bottom_button(BottomButton::Main)
530    }
531
532    /// Legacy alias for [`Self::set_bottom_button_text`] with
533    /// [`BottomButton::Main`].
534    pub fn set_main_button_text(&self, text: &str) -> Result<(), JsValue> {
535        self.set_bottom_button_text(BottomButton::Main, text)
536    }
537
538    /// Legacy alias for [`Self::set_bottom_button_color`] with
539    /// [`BottomButton::Main`].
540    pub fn set_main_button_color(&self, color: &str) -> Result<(), JsValue> {
541        self.set_bottom_button_color(BottomButton::Main, color)
542    }
543
544    /// Legacy alias for [`Self::set_bottom_button_text_color`] with
545    /// [`BottomButton::Main`].
546    pub fn set_main_button_text_color(&self, color: &str) -> Result<(), JsValue> {
547        self.set_bottom_button_text_color(BottomButton::Main, color)
548    }
549
550    /// Set custom emoji icon for the main button (Bot API 9.5+).
551    ///
552    /// Legacy alias for [`Self::set_bottom_button_icon_custom_emoji_id`] with
553    /// [`BottomButton::Main`].
554    ///
555    /// # Examples
556    /// ```no_run
557    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
558    ///
559    /// if let Some(app) = TelegramWebApp::instance() {
560    ///     let _ = app.set_main_button_icon_custom_emoji_id("123456789");
561    /// }
562    /// ```
563    pub fn set_main_button_icon_custom_emoji_id(&self, icon_id: &str) -> Result<(), JsValue> {
564        self.set_bottom_button_icon_custom_emoji_id(BottomButton::Main, icon_id)
565    }
566
567    /// Enable the main bottom button.
568    ///
569    /// # Examples
570    /// ```no_run
571    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
572    ///
573    /// if let Some(app) = TelegramWebApp::instance() {
574    ///     let _ = app.enable_main_button();
575    /// }
576    /// ```
577    pub fn enable_main_button(&self) -> Result<(), JsValue> {
578        self.enable_bottom_button(BottomButton::Main)
579    }
580
581    /// Disable the main bottom button.
582    ///
583    /// # Examples
584    /// ```no_run
585    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
586    ///
587    /// if let Some(app) = TelegramWebApp::instance() {
588    ///     let _ = app.disable_main_button();
589    /// }
590    /// ```
591    pub fn disable_main_button(&self) -> Result<(), JsValue> {
592        self.disable_bottom_button(BottomButton::Main)
593    }
594
595    /// Show progress on the main bottom button.
596    ///
597    /// # Examples
598    /// ```no_run
599    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
600    ///
601    /// if let Some(app) = TelegramWebApp::instance() {
602    ///     let _ = app.show_main_button_progress(false);
603    /// }
604    /// ```
605    pub fn show_main_button_progress(&self, leave_active: bool) -> Result<(), JsValue> {
606        self.show_bottom_button_progress(BottomButton::Main, leave_active)
607    }
608
609    /// Hide progress indicator from the main bottom button.
610    ///
611    /// # Examples
612    /// ```no_run
613    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
614    ///
615    /// if let Some(app) = TelegramWebApp::instance() {
616    ///     let _ = app.hide_main_button_progress();
617    /// }
618    /// ```
619    pub fn hide_main_button_progress(&self) -> Result<(), JsValue> {
620        self.hide_bottom_button_progress(BottomButton::Main)
621    }
622
623    /// Update the main button state via
624    /// [`set_bottom_button_params`](Self::set_bottom_button_params).
625    pub fn set_main_button_params(&self, params: &BottomButtonParams<'_>) -> Result<(), JsValue> {
626        self.set_bottom_button_params(BottomButton::Main, params)
627    }
628
629    /// Legacy alias for [`Self::set_bottom_button_callback`] with
630    /// [`BottomButton::Main`].
631    pub fn set_main_button_callback<F>(
632        &self,
633        callback: F
634    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
635    where
636        F: 'static + Fn()
637    {
638        self.set_bottom_button_callback(BottomButton::Main, callback)
639    }
640
641    /// Remove callback for the main button.
642    ///
643    /// Legacy alias for [`Self::remove_bottom_button_callback`].
644    pub fn remove_main_button_callback(
645        &self,
646        handle: EventHandle<dyn FnMut()>
647    ) -> Result<(), JsValue> {
648        self.remove_bottom_button_callback(handle)
649    }
650
651    // === Secondary button convenience methods ===
652
653    /// Show the secondary bottom button.
654    pub fn show_secondary_button(&self) -> Result<(), JsValue> {
655        self.show_bottom_button(BottomButton::Secondary)
656    }
657
658    /// Hide the secondary bottom button.
659    pub fn hide_secondary_button(&self) -> Result<(), JsValue> {
660        self.hide_bottom_button(BottomButton::Secondary)
661    }
662
663    /// Set text for the secondary bottom button.
664    pub fn set_secondary_button_text(&self, text: &str) -> Result<(), JsValue> {
665        self.set_bottom_button_text(BottomButton::Secondary, text)
666    }
667
668    /// Set color for the secondary bottom button.
669    pub fn set_secondary_button_color(&self, color: &str) -> Result<(), JsValue> {
670        self.set_bottom_button_color(BottomButton::Secondary, color)
671    }
672
673    /// Set text color for the secondary bottom button.
674    pub fn set_secondary_button_text_color(&self, color: &str) -> Result<(), JsValue> {
675        self.set_bottom_button_text_color(BottomButton::Secondary, color)
676    }
677
678    /// Set custom emoji icon for the secondary button (Bot API 9.5+).
679    ///
680    /// Convenience alias for [`Self::set_bottom_button_icon_custom_emoji_id`]
681    /// with [`BottomButton::Secondary`].
682    ///
683    /// # Examples
684    /// ```no_run
685    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
686    ///
687    /// if let Some(app) = TelegramWebApp::instance() {
688    ///     let _ = app.set_secondary_button_icon_custom_emoji_id("123456789");
689    /// }
690    /// ```
691    pub fn set_secondary_button_icon_custom_emoji_id(&self, icon_id: &str) -> Result<(), JsValue> {
692        self.set_bottom_button_icon_custom_emoji_id(BottomButton::Secondary, icon_id)
693    }
694
695    /// Enable the secondary bottom button.
696    ///
697    /// # Examples
698    /// ```no_run
699    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
700    ///
701    /// if let Some(app) = TelegramWebApp::instance() {
702    ///     let _ = app.enable_secondary_button();
703    /// }
704    /// ```
705    pub fn enable_secondary_button(&self) -> Result<(), JsValue> {
706        self.enable_bottom_button(BottomButton::Secondary)
707    }
708
709    /// Disable the secondary bottom button.
710    ///
711    /// # Examples
712    /// ```no_run
713    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
714    ///
715    /// if let Some(app) = TelegramWebApp::instance() {
716    ///     let _ = app.disable_secondary_button();
717    /// }
718    /// ```
719    pub fn disable_secondary_button(&self) -> Result<(), JsValue> {
720        self.disable_bottom_button(BottomButton::Secondary)
721    }
722
723    /// Show progress on the secondary bottom button.
724    ///
725    /// # Examples
726    /// ```no_run
727    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
728    ///
729    /// if let Some(app) = TelegramWebApp::instance() {
730    ///     let _ = app.show_secondary_button_progress(false);
731    /// }
732    /// ```
733    pub fn show_secondary_button_progress(&self, leave_active: bool) -> Result<(), JsValue> {
734        self.show_bottom_button_progress(BottomButton::Secondary, leave_active)
735    }
736
737    /// Hide progress indicator from the secondary bottom button.
738    ///
739    /// # Examples
740    /// ```no_run
741    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
742    ///
743    /// if let Some(app) = TelegramWebApp::instance() {
744    ///     let _ = app.hide_secondary_button_progress();
745    /// }
746    /// ```
747    pub fn hide_secondary_button_progress(&self) -> Result<(), JsValue> {
748        self.hide_bottom_button_progress(BottomButton::Secondary)
749    }
750
751    /// Set callback for the secondary bottom button.
752    pub fn set_secondary_button_callback<F>(
753        &self,
754        callback: F
755    ) -> Result<EventHandle<dyn FnMut()>, JsValue>
756    where
757        F: 'static + Fn()
758    {
759        self.set_bottom_button_callback(BottomButton::Secondary, callback)
760    }
761
762    /// Remove callback for the secondary bottom button.
763    pub fn remove_secondary_button_callback(
764        &self,
765        handle: EventHandle<dyn FnMut()>
766    ) -> Result<(), JsValue> {
767        self.remove_bottom_button_callback(handle)
768    }
769
770    /// Hide the on-screen keyboard.
771    /// Call `WebApp.hideKeyboard()`.
772    ///
773    /// # Examples
774    /// ```no_run
775    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
776    /// # let app = TelegramWebApp::instance().unwrap();
777    /// app.hide_keyboard().unwrap();
778    /// ```
779    ///
780    /// # Errors
781    /// Returns [`JsValue`] if the underlying JS call fails.
782    pub fn hide_keyboard(&self) -> Result<(), JsValue> {
783        self.call0("hideKeyboard")
784    }
785}