Skip to main content

telegram_webapp_sdk/webapp/
dialogs.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use js_sys::{Function, Object, Reflect};
5use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
6
7use crate::webapp::{
8    TelegramWebApp,
9    core::{await_one_shot, one_shot_promise}
10};
11
12impl TelegramWebApp {
13    /// Call `WebApp.showAlert(message)`.
14    ///
15    /// # Errors
16    /// Returns [`JsValue`] if the underlying JS call fails.
17    pub fn show_alert(&self, msg: &str) -> Result<(), JsValue> {
18        self.call1("showAlert", &msg.into())
19    }
20
21    /// Callback variant of [`Self::show_confirm`].
22    ///
23    /// # Errors
24    /// Returns [`JsValue`] if the underlying JS call fails.
25    pub fn show_confirm_with_callback<F>(&self, msg: &str, on_confirm: F) -> Result<(), JsValue>
26    where
27        F: 'static + FnOnce(bool)
28    {
29        let cb = Closure::once_into_js(move |v: JsValue| {
30            on_confirm(v.as_bool().unwrap_or(false));
31        });
32        let f = Reflect::get(&self.inner, &"showConfirm".into())?;
33        let func = f
34            .dyn_ref::<Function>()
35            .ok_or_else(|| JsValue::from_str("showConfirm is not a function"))?;
36        func.call2(&self.inner, &msg.into(), &cb)?;
37        Ok(())
38    }
39
40    /// Async wrapper over `WebApp.showConfirm`. Resolves with the user's
41    /// boolean answer.
42    ///
43    /// # Errors
44    /// Returns [`JsValue`] if the underlying JS call fails.
45    pub async fn show_confirm(&self, msg: &str) -> Result<bool, JsValue> {
46        let webapp = self.inner.clone();
47        let msg = msg.to_owned();
48        let promise = one_shot_promise(move |resolve, _reject| {
49            let cb = Closure::once_into_js(move |v: JsValue| {
50                let _ = resolve.call1(&JsValue::NULL, &v);
51            });
52            let f = Reflect::get(&webapp, &"showConfirm".into())?;
53            let func = f
54                .dyn_ref::<Function>()
55                .ok_or_else(|| JsValue::from_str("showConfirm is not a function"))?;
56            func.call2(&webapp, &msg.into(), &cb)?;
57            Ok(())
58        });
59        let value = await_one_shot(promise).await?;
60        Ok(value.as_bool().unwrap_or(false))
61    }
62
63    /// Call `WebApp.showPopup(params, callback)`.
64    ///
65    /// # Examples
66    /// ```no_run
67    /// # use js_sys::Object;
68    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
69    /// # let app = TelegramWebApp::instance().unwrap();
70    /// let params = Object::new();
71    /// app.show_popup_with_callback(&params.into(), |id| {
72    ///     let _ = id;
73    /// })
74    /// .unwrap();
75    /// ```
76    /// Callback variant of [`Self::show_popup`].
77    pub fn show_popup_with_callback<F>(&self, params: &JsValue, callback: F) -> Result<(), JsValue>
78    where
79        F: 'static + FnOnce(String)
80    {
81        let cb = Closure::once_into_js(move |id: JsValue| {
82            callback(id.as_string().unwrap_or_default());
83        });
84        Reflect::get(&self.inner, &"showPopup".into())?
85            .dyn_into::<Function>()?
86            .call2(&self.inner, params, &cb)?;
87        Ok(())
88    }
89
90    /// Async wrapper over `WebApp.showPopup`. Resolves with the id of the
91    /// button the user pressed, or an empty string if the popup was dismissed.
92    ///
93    /// # Errors
94    /// Returns [`JsValue`] if the underlying JS call fails.
95    pub async fn show_popup(&self, params: &JsValue) -> Result<String, JsValue> {
96        let webapp = self.inner.clone();
97        let params = params.clone();
98        let promise = one_shot_promise(move |resolve, _reject| {
99            let cb = Closure::once_into_js(move |id: JsValue| {
100                let _ = resolve.call1(&JsValue::NULL, &id);
101            });
102            Reflect::get(&webapp, &"showPopup".into())?
103                .dyn_into::<Function>()?
104                .call2(&webapp, &params, &cb)?;
105            Ok(())
106        });
107        let value = await_one_shot(promise).await?;
108        Ok(value.as_string().unwrap_or_default())
109    }
110
111    /// Call `WebApp.showScanQrPopup({ text }, callback)`.
112    ///
113    /// The text is shown above the scanner viewport. Pass an empty string to
114    /// open the scanner without a caption.
115    ///
116    /// # Examples
117    /// ```no_run
118    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
119    /// # let app = TelegramWebApp::instance().unwrap();
120    /// app.show_scan_qr_popup_with_callback("Scan", |text| {
121    ///     let _ = text;
122    /// })
123    /// .unwrap();
124    /// ```
125    /// Callback variant of [`Self::show_scan_qr_popup`].
126    pub fn show_scan_qr_popup_with_callback<F>(
127        &self,
128        text: &str,
129        callback: F
130    ) -> Result<(), JsValue>
131    where
132        F: 'static + FnOnce(String)
133    {
134        let cb = Closure::once_into_js(move |value: JsValue| {
135            callback(value.as_string().unwrap_or_default());
136        });
137        let params = Object::new();
138        Reflect::set(&params, &"text".into(), &text.into())?;
139        Reflect::get(&self.inner, &"showScanQrPopup".into())?
140            .dyn_into::<Function>()?
141            .call2(&self.inner, &params, &cb)?;
142        Ok(())
143    }
144
145    /// Async wrapper over `WebApp.showScanQrPopup`. Resolves with the scanned
146    /// text. Pass an empty `text` to open the scanner without a caption.
147    ///
148    /// # Errors
149    /// Returns [`JsValue`] if the underlying JS call fails.
150    pub async fn show_scan_qr_popup(&self, text: &str) -> Result<String, JsValue> {
151        let webapp = self.inner.clone();
152        let text = text.to_owned();
153        let promise = one_shot_promise(move |resolve, _reject| {
154            let cb = Closure::once_into_js(move |value: JsValue| {
155                let _ = resolve.call1(&JsValue::NULL, &value);
156            });
157            let params = Object::new();
158            Reflect::set(&params, &"text".into(), &text.into())?;
159            Reflect::get(&webapp, &"showScanQrPopup".into())?
160                .dyn_into::<Function>()?
161                .call2(&webapp, &params, &cb)?;
162            Ok(())
163        });
164        let value = await_one_shot(promise).await?;
165        Ok(value.as_string().unwrap_or_default())
166    }
167
168    /// Call `WebApp.closeScanQrPopup()`.
169    ///
170    /// # Examples
171    /// ```no_run
172    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
173    /// # let app = TelegramWebApp::instance().unwrap();
174    /// app.close_scan_qr_popup().unwrap();
175    /// ```
176    pub fn close_scan_qr_popup(&self) -> Result<(), JsValue> {
177        Reflect::get(&self.inner, &"closeScanQrPopup".into())?
178            .dyn_into::<Function>()?
179            .call0(&self.inner)?;
180        Ok(())
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use js_sys::{Function, Object, Reflect};
187    use wasm_bindgen::JsValue;
188    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
189    use web_sys::window;
190
191    use crate::webapp::TelegramWebApp;
192
193    wasm_bindgen_test_configure!(run_in_browser);
194
195    fn setup_webapp() -> Object {
196        let win = window().expect("window");
197        let telegram = Object::new();
198        let webapp = Object::new();
199        let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
200        let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
201        webapp
202    }
203
204    #[wasm_bindgen_test]
205    #[allow(dead_code, clippy::unused_unit)]
206    fn show_scan_qr_popup_passes_params_as_object_with_text() {
207        let webapp = setup_webapp();
208        let capture = Function::new_with_args("params, _cb", "this.captured_params = params;");
209        let _ = Reflect::set(&webapp, &"showScanQrPopup".into(), &capture);
210
211        let app = TelegramWebApp::instance().expect("instance");
212        app.show_scan_qr_popup_with_callback("Scan", |_| {})
213            .expect("ok");
214
215        let params = Reflect::get(&webapp, &"captured_params".into()).expect("captured");
216        assert!(!params.is_undefined(), "scan params must be an object");
217        let text = Reflect::get(&params, &"text".into())
218            .expect("text field")
219            .as_string();
220        assert_eq!(text.as_deref(), Some("Scan"));
221    }
222
223    #[wasm_bindgen_test]
224    #[allow(dead_code, clippy::unused_unit)]
225    fn show_alert_passes_message() {
226        let webapp = setup_webapp();
227        let capture = Function::new_with_args("msg", "this.captured_alert = msg;");
228        let _ = Reflect::set(&webapp, &"showAlert".into(), &capture);
229
230        let app = TelegramWebApp::instance().expect("instance");
231        app.show_alert("Heads up").expect("ok");
232
233        assert_eq!(
234            Reflect::get(&webapp, &"captured_alert".into())
235                .unwrap()
236                .as_string()
237                .as_deref(),
238            Some("Heads up")
239        );
240    }
241
242    #[wasm_bindgen_test]
243    #[allow(dead_code, clippy::unused_unit)]
244    fn show_confirm_passes_message_and_routes_boolean_back() {
245        let webapp = setup_webapp();
246        let invoke = Function::new_with_args("msg, cb", "this.captured_confirm = msg; cb(true);");
247        let _ = Reflect::set(&webapp, &"showConfirm".into(), &invoke);
248
249        let app = TelegramWebApp::instance().expect("instance");
250        let received = std::rc::Rc::new(std::cell::Cell::new(false));
251        let received_ref = received.clone();
252        app.show_confirm_with_callback("Proceed?", move |ok| received_ref.set(ok))
253            .expect("ok");
254
255        assert_eq!(
256            Reflect::get(&webapp, &"captured_confirm".into())
257                .unwrap()
258                .as_string()
259                .as_deref(),
260            Some("Proceed?")
261        );
262        assert!(received.get());
263    }
264
265    #[wasm_bindgen_test]
266    #[allow(dead_code, clippy::unused_unit)]
267    fn close_scan_qr_popup_calls_js() {
268        let webapp = setup_webapp();
269        let called = std::rc::Rc::new(std::cell::Cell::new(false));
270        let called_ref = called.clone();
271        let close =
272            wasm_bindgen::closure::Closure::<dyn FnMut()>::new(move || called_ref.set(true));
273        let _ = Reflect::set(
274            &webapp,
275            &"closeScanQrPopup".into(),
276            wasm_bindgen::JsCast::unchecked_ref::<Function>(close.as_ref())
277        );
278        close.forget();
279
280        let app = TelegramWebApp::instance().expect("instance");
281        app.close_scan_qr_popup().expect("ok");
282        assert!(called.get());
283    }
284
285    #[wasm_bindgen_test]
286    #[allow(dead_code, clippy::unused_unit)]
287    fn show_scan_qr_popup_callback_receives_scanned_text() {
288        let webapp = setup_webapp();
289        // Synchronously invoke the callback with a scanned value so we can
290        // observe it without scheduling.
291        let invoke = Function::new_with_args("_params, cb", "cb('payload');");
292        let _ = Reflect::set(&webapp, &"showScanQrPopup".into(), &invoke);
293
294        let app = TelegramWebApp::instance().expect("instance");
295        let captured = std::rc::Rc::new(std::cell::RefCell::new(String::new()));
296        let captured_ref = captured.clone();
297        app.show_scan_qr_popup_with_callback("", move |t| {
298            *captured_ref.borrow_mut() = t;
299        })
300        .expect("ok");
301
302        assert_eq!(captured.borrow().as_str(), "payload");
303        let _ = JsValue::null();
304    }
305}