1use js_sys::{Function, Object, Reflect};
2use serde::Serialize;
3use serde_wasm_bindgen::to_value;
4use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
5use web_sys::window;
6
7use crate::{
8 core::types::download_file_params::DownloadFileParams,
9 logger,
10 validate_init_data::{self, ValidationKey}
11};
12
13pub struct EventHandle<T: ?Sized> {
15 target: Object,
16 method: &'static str,
17 event: Option<String>,
18 callback: Closure<T>
19}
20
21impl<T: ?Sized> EventHandle<T> {
22 fn new(
23 target: Object,
24 method: &'static str,
25 event: Option<String>,
26 callback: Closure<T>
27 ) -> Self {
28 Self {
29 target,
30 method,
31 event,
32 callback
33 }
34 }
35
36 pub(crate) fn unregister(self) -> Result<(), JsValue> {
37 let f = Reflect::get(&self.target, &self.method.into())?;
38 let func = f
39 .dyn_ref::<Function>()
40 .ok_or_else(|| JsValue::from_str(&format!("{} is not a function", self.method)))?;
41 match self.event {
42 Some(event) => func.call2(
43 &self.target,
44 &event.into(),
45 self.callback.as_ref().unchecked_ref()
46 )?,
47 None => func.call1(&self.target, self.callback.as_ref().unchecked_ref())?
48 };
49 Ok(())
50 }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
55pub enum BottomButton {
56 Main,
58 Secondary
60}
61
62impl BottomButton {
63 const fn js_name(self) -> &'static str {
64 match self {
65 BottomButton::Main => "MainButton",
66 BottomButton::Secondary => "SecondaryButton"
67 }
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
85#[serde(rename_all = "lowercase")]
86pub enum SecondaryButtonPosition {
87 Top,
89 Left,
91 Bottom,
93 Right
95}
96
97impl SecondaryButtonPosition {
98 fn from_js_value(value: JsValue) -> Option<Self> {
99 let as_str = value.as_string()?;
100 match as_str.as_str() {
101 "top" => Some(Self::Top),
102 "left" => Some(Self::Left),
103 "bottom" => Some(Self::Bottom),
104 "right" => Some(Self::Right),
105 _ => None
106 }
107 }
108}
109
110#[derive(Clone, Copy, Debug, PartialEq)]
128pub struct SafeAreaInset {
129 pub top: f64,
131 pub bottom: f64,
133 pub left: f64,
135 pub right: f64
137}
138
139impl SafeAreaInset {
140 fn from_js(value: JsValue) -> Option<Self> {
141 let object = value.dyn_into::<Object>().ok()?;
142 let top = Reflect::get(&object, &"top".into()).ok()?.as_f64()?;
143 let bottom = Reflect::get(&object, &"bottom".into()).ok()?.as_f64()?;
144 let left = Reflect::get(&object, &"left".into()).ok()?.as_f64()?;
145 let right = Reflect::get(&object, &"right".into()).ok()?.as_f64()?;
146 Some(Self {
147 top,
148 bottom,
149 left,
150 right
151 })
152 }
153}
154
155#[derive(Debug, Default, Serialize)]
171pub struct BottomButtonParams<'a> {
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub text: Option<&'a str>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub color: Option<&'a str>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub text_color: Option<&'a str>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub is_active: Option<bool>,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub is_visible: Option<bool>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub has_shine_effect: Option<bool>
184}
185
186#[derive(Debug, Default, Serialize)]
203pub struct SecondaryButtonParams<'a> {
204 #[serde(flatten)]
205 pub common: BottomButtonParams<'a>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub position: Option<SecondaryButtonPosition>
208}
209
210#[derive(Debug, Default, Serialize)]
224pub struct OpenLinkOptions {
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub try_instant_view: Option<bool>
227}
228
229#[derive(Clone, Copy, Debug)]
232pub enum BackgroundEvent {
233 MainButtonClicked,
235 BackButtonClicked,
237 SettingsButtonClicked,
239 WriteAccessRequested,
241 ContactRequested,
243 PhoneRequested,
245 InvoiceClosed,
247 PopupClosed,
249 QrTextReceived,
251 ClipboardTextReceived
253}
254
255impl BackgroundEvent {
256 const fn as_str(self) -> &'static str {
257 match self {
258 BackgroundEvent::MainButtonClicked => "mainButtonClicked",
259 BackgroundEvent::BackButtonClicked => "backButtonClicked",
260 BackgroundEvent::SettingsButtonClicked => "settingsButtonClicked",
261 BackgroundEvent::WriteAccessRequested => "writeAccessRequested",
262 BackgroundEvent::ContactRequested => "contactRequested",
263 BackgroundEvent::PhoneRequested => "phoneRequested",
264 BackgroundEvent::InvoiceClosed => "invoiceClosed",
265 BackgroundEvent::PopupClosed => "popupClosed",
266 BackgroundEvent::QrTextReceived => "qrTextReceived",
267 BackgroundEvent::ClipboardTextReceived => "clipboardTextReceived"
268 }
269 }
270}
271
272#[derive(Clone)]
274pub struct TelegramWebApp {
275 inner: Object
276}
277
278impl TelegramWebApp {
279 pub fn instance() -> Option<Self> {
281 let win = window()?;
282 let tg = Reflect::get(&win, &"Telegram".into()).ok()?;
283 let webapp = Reflect::get(&tg, &"WebApp".into()).ok()?;
284 webapp.dyn_into::<Object>().ok().map(|inner| Self {
285 inner
286 })
287 }
288
289 pub fn try_instance() -> Result<Self, JsValue> {
295 let win = window().ok_or_else(|| JsValue::from_str("window not available"))?;
296 let tg = Reflect::get(&win, &"Telegram".into())?;
297 let webapp = Reflect::get(&tg, &"WebApp".into())?;
298 let inner = webapp.dyn_into::<Object>()?;
299 Ok(Self {
300 inner
301 })
302 }
303
304 pub fn validate_init_data(
321 init_data: &str,
322 key: ValidationKey
323 ) -> Result<(), validate_init_data::ValidationError> {
324 match key {
325 ValidationKey::BotToken(token) => {
326 validate_init_data::verify_hmac_sha256(init_data, token)
327 }
328 ValidationKey::Ed25519PublicKey(pk) => {
329 validate_init_data::verify_ed25519(init_data, pk)
330 }
331 }
332 }
333
334 pub fn send_data(&self, data: &str) -> Result<(), JsValue> {
339 self.call1("sendData", &data.into())
340 }
341
342 pub fn expand(&self) -> Result<(), JsValue> {
347 self.call0("expand")
348 }
349
350 pub fn close(&self) -> Result<(), JsValue> {
355 self.call0("close")
356 }
357
358 pub fn enable_closing_confirmation(&self) -> Result<(), JsValue> {
370 self.call0("enableClosingConfirmation")
371 }
372
373 pub fn disable_closing_confirmation(&self) -> Result<(), JsValue> {
385 self.call0("disableClosingConfirmation")
386 }
387
388 pub fn is_closing_confirmation_enabled(&self) -> bool {
397 Reflect::get(&self.inner, &"isClosingConfirmationEnabled".into())
398 .ok()
399 .and_then(|v| v.as_bool())
400 .unwrap_or(false)
401 }
402
403 pub fn request_fullscreen(&self) -> Result<(), JsValue> {
415 self.call0("requestFullscreen")
416 }
417
418 pub fn exit_fullscreen(&self) -> Result<(), JsValue> {
430 self.call0("exitFullscreen")
431 }
432
433 pub fn lock_orientation(&self, orientation: &str) -> Result<(), JsValue> {
445 self.call1("lockOrientation", &orientation.into())
446 }
447
448 pub fn unlock_orientation(&self) -> Result<(), JsValue> {
460 self.call0("unlockOrientation")
461 }
462
463 pub fn enable_vertical_swipes(&self) -> Result<(), JsValue> {
475 self.call0("enableVerticalSwipes")
476 }
477
478 pub fn disable_vertical_swipes(&self) -> Result<(), JsValue> {
490 self.call0("disableVerticalSwipes")
491 }
492
493 pub fn show_alert(&self, msg: &str) -> Result<(), JsValue> {
498 self.call1("showAlert", &msg.into())
499 }
500
501 pub fn show_confirm<F>(&self, msg: &str, on_confirm: F) -> Result<(), JsValue>
506 where
507 F: 'static + Fn(bool)
508 {
509 let cb = Closure::<dyn FnMut(bool)>::new(on_confirm);
510 let f = Reflect::get(&self.inner, &"showConfirm".into())?;
511 let func = f
512 .dyn_ref::<Function>()
513 .ok_or_else(|| JsValue::from_str("showConfirm is not a function"))?;
514 func.call2(&self.inner, &msg.into(), cb.as_ref().unchecked_ref())?;
515 cb.forget(); Ok(())
517 }
518
519 pub fn open_link(&self, url: &str, options: Option<&OpenLinkOptions>) -> Result<(), JsValue> {
528 let f = Reflect::get(&self.inner, &"openLink".into())?;
529 let func = f
530 .dyn_ref::<Function>()
531 .ok_or_else(|| JsValue::from_str("openLink is not a function"))?;
532 match options {
533 Some(opts) => {
534 let value = to_value(opts).map_err(|err| JsValue::from_str(&err.to_string()))?;
535 func.call2(&self.inner, &url.into(), &value)?;
536 }
537 None => {
538 func.call1(&self.inner, &url.into())?;
539 }
540 }
541 Ok(())
542 }
543
544 pub fn open_telegram_link(&self, url: &str) -> Result<(), JsValue> {
553 Reflect::get(&self.inner, &"openTelegramLink".into())?
554 .dyn_into::<Function>()?
555 .call1(&self.inner, &url.into())?;
556 Ok(())
557 }
558
559 pub fn is_version_at_least(&self, version: &str) -> Result<bool, JsValue> {
570 let f = Reflect::get(&self.inner, &"isVersionAtLeast".into())?;
571 let func = f
572 .dyn_ref::<Function>()
573 .ok_or_else(|| JsValue::from_str("isVersionAtLeast is not a function"))?;
574 let result = func.call1(&self.inner, &version.into())?;
575 Ok(result.as_bool().unwrap_or(false))
576 }
577
578 pub fn open_invoice<F>(&self, url: &str, callback: F) -> Result<(), JsValue>
590 where
591 F: 'static + Fn(String)
592 {
593 let cb = Closure::<dyn FnMut(JsValue)>::new(move |status: JsValue| {
594 callback(status.as_string().unwrap_or_default());
595 });
596 Reflect::get(&self.inner, &"openInvoice".into())?
597 .dyn_into::<Function>()?
598 .call2(&self.inner, &url.into(), cb.as_ref().unchecked_ref())?;
599 cb.forget();
600 Ok(())
601 }
602
603 pub fn switch_inline_query(
615 &self,
616 query: &str,
617 choose_chat_types: Option<&JsValue>
618 ) -> Result<(), JsValue> {
619 let f = Reflect::get(&self.inner, &"switchInlineQuery".into())?;
620 let func = f
621 .dyn_ref::<Function>()
622 .ok_or_else(|| JsValue::from_str("switchInlineQuery is not a function"))?;
623 match choose_chat_types {
624 Some(types) => func.call2(&self.inner, &query.into(), types)?,
625 None => func.call1(&self.inner, &query.into())?
626 };
627 Ok(())
628 }
629
630 pub fn share_message<F>(&self, msg_id: &str, callback: F) -> Result<(), JsValue>
645 where
646 F: 'static + Fn(bool)
647 {
648 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
649 callback(v.as_bool().unwrap_or(false));
650 });
651 let f = Reflect::get(&self.inner, &"shareMessage".into())?;
652 let func = f
653 .dyn_ref::<Function>()
654 .ok_or_else(|| JsValue::from_str("shareMessage is not a function"))?;
655 func.call2(&self.inner, &msg_id.into(), cb.as_ref().unchecked_ref())?;
656 cb.forget();
657 Ok(())
658 }
659
660 pub fn share_to_story(
675 &self,
676 media_url: &str,
677 params: Option<&JsValue>
678 ) -> Result<(), JsValue> {
679 let f = Reflect::get(&self.inner, &"shareToStory".into())?;
680 let func = f
681 .dyn_ref::<Function>()
682 .ok_or_else(|| JsValue::from_str("shareToStory is not a function"))?;
683 match params {
684 Some(p) => func.call2(&self.inner, &media_url.into(), p)?,
685 None => func.call1(&self.inner, &media_url.into())?
686 };
687 Ok(())
688 }
689
690 pub fn share_url(&self, url: &str, text: Option<&str>) -> Result<(), JsValue> {
703 let f = Reflect::get(&self.inner, &"shareURL".into())?;
704 let func = f
705 .dyn_ref::<Function>()
706 .ok_or_else(|| JsValue::from_str("shareURL is not a function"))?;
707 match text {
708 Some(t) => func.call2(&self.inner, &url.into(), &t.into())?,
709 None => func.call1(&self.inner, &url.into())?
710 };
711 Ok(())
712 }
713
714 pub fn join_voice_chat(
726 &self,
727 chat_id: &str,
728 invite_hash: Option<&str>
729 ) -> Result<(), JsValue> {
730 let f = Reflect::get(&self.inner, &"joinVoiceChat".into())?;
731 let func = f
732 .dyn_ref::<Function>()
733 .ok_or_else(|| JsValue::from_str("joinVoiceChat is not a function"))?;
734 match invite_hash {
735 Some(hash) => func.call2(&self.inner, &chat_id.into(), &hash.into())?,
736 None => func.call1(&self.inner, &chat_id.into())?
737 };
738 Ok(())
739 }
740
741 pub fn add_to_home_screen(&self) -> Result<bool, JsValue> {
750 let f = Reflect::get(&self.inner, &"addToHomeScreen".into())?;
751 let func = f
752 .dyn_ref::<Function>()
753 .ok_or_else(|| JsValue::from_str("addToHomeScreen is not a function"))?;
754 let result = func.call0(&self.inner)?;
755 Ok(result.as_bool().unwrap_or(false))
756 }
757
758 pub fn check_home_screen_status<F>(&self, callback: F) -> Result<(), JsValue>
770 where
771 F: 'static + Fn(String)
772 {
773 let cb = Closure::<dyn FnMut(JsValue)>::new(move |status: JsValue| {
774 callback(status.as_string().unwrap_or_default());
775 });
776 let f = Reflect::get(&self.inner, &"checkHomeScreenStatus".into())?;
777 let func = f
778 .dyn_ref::<Function>()
779 .ok_or_else(|| JsValue::from_str("checkHomeScreenStatus is not a function"))?;
780 func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
781 cb.forget();
782 Ok(())
783 }
784
785 pub fn request_write_access<F>(&self, callback: F) -> Result<(), JsValue>
800 where
801 F: 'static + Fn(bool)
802 {
803 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
804 callback(v.as_bool().unwrap_or(false));
805 });
806 self.call1("requestWriteAccess", cb.as_ref().unchecked_ref())?;
807 cb.forget();
808 Ok(())
809 }
810
811 pub fn download_file<F>(
833 &self,
834 params: DownloadFileParams<'_>,
835 callback: F
836 ) -> Result<(), JsValue>
837 where
838 F: 'static + Fn(String)
839 {
840 let js_params =
841 to_value(¶ms).map_err(|e| JsValue::from_str(&format!("serialize params: {e}")))?;
842 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
843 callback(v.as_string().unwrap_or_default());
844 });
845 Reflect::get(&self.inner, &"downloadFile".into())?
846 .dyn_into::<Function>()?
847 .call2(&self.inner, &js_params, cb.as_ref().unchecked_ref())?;
848 cb.forget();
849 Ok(())
850 }
851
852 pub fn request_emoji_status_access<F>(&self, callback: F) -> Result<(), JsValue>
867 where
868 F: 'static + Fn(bool)
869 {
870 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
871 callback(v.as_bool().unwrap_or(false));
872 });
873 let f = Reflect::get(&self.inner, &"requestEmojiStatusAccess".into())?;
874 let func = f
875 .dyn_ref::<Function>()
876 .ok_or_else(|| JsValue::from_str("requestEmojiStatusAccess is not a function"))?;
877 func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
878 cb.forget();
879 Ok(())
880 }
881
882 pub fn set_emoji_status<F>(&self, status: &JsValue, callback: F) -> Result<(), JsValue>
901 where
902 F: 'static + Fn(bool)
903 {
904 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
905 callback(v.as_bool().unwrap_or(false));
906 });
907 let f = Reflect::get(&self.inner, &"setEmojiStatus".into())?;
908 let func = f
909 .dyn_ref::<Function>()
910 .ok_or_else(|| JsValue::from_str("setEmojiStatus is not a function"))?;
911 func.call2(&self.inner, status, cb.as_ref().unchecked_ref())?;
912 cb.forget();
913 Ok(())
914 }
915
916 pub fn show_popup<F>(&self, params: &JsValue, callback: F) -> Result<(), JsValue>
930 where
931 F: 'static + Fn(String)
932 {
933 let cb = Closure::<dyn FnMut(JsValue)>::new(move |id: JsValue| {
934 callback(id.as_string().unwrap_or_default());
935 });
936 Reflect::get(&self.inner, &"showPopup".into())?
937 .dyn_into::<Function>()?
938 .call2(&self.inner, params, cb.as_ref().unchecked_ref())?;
939 cb.forget();
940 Ok(())
941 }
942
943 pub fn show_scan_qr_popup<F>(&self, text: &str, callback: F) -> Result<(), JsValue>
955 where
956 F: 'static + Fn(String)
957 {
958 let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
959 callback(value.as_string().unwrap_or_default());
960 });
961 Reflect::get(&self.inner, &"showScanQrPopup".into())?
962 .dyn_into::<Function>()?
963 .call2(&self.inner, &text.into(), cb.as_ref().unchecked_ref())?;
964 cb.forget();
965 Ok(())
966 }
967
968 pub fn close_scan_qr_popup(&self) -> Result<(), JsValue> {
977 Reflect::get(&self.inner, &"closeScanQrPopup".into())?
978 .dyn_into::<Function>()?
979 .call0(&self.inner)?;
980 Ok(())
981 }
982
983 fn bottom_button_object(&self, button: BottomButton) -> Result<Object, JsValue> {
985 let name = button.js_name();
986 Reflect::get(&self.inner, &name.into())
987 .inspect_err(|_| logger::error(&format!("{name} not available")))?
988 .dyn_into::<Object>()
989 .inspect_err(|_| logger::error(&format!("{name} is not an object")))
990 }
991
992 fn bottom_button_method(
993 &self,
994 button: BottomButton,
995 method: &str,
996 arg: Option<&JsValue>
997 ) -> Result<(), JsValue> {
998 let name = button.js_name();
999 let btn = self.bottom_button_object(button)?;
1000 let f = Reflect::get(&btn, &method.into())
1001 .inspect_err(|_| logger::error(&format!("{name}.{method} not available")))?;
1002 let func = f.dyn_ref::<Function>().ok_or_else(|| {
1003 logger::error(&format!("{name}.{method} is not a function"));
1004 JsValue::from_str("not a function")
1005 })?;
1006 let result = match arg {
1007 Some(v) => func.call1(&btn, v),
1008 None => func.call0(&btn)
1009 };
1010 result.inspect_err(|_| logger::error(&format!("{name}.{method} call failed")))?;
1011 Ok(())
1012 }
1013
1014 fn bottom_button_property(&self, button: BottomButton, property: &str) -> Option<JsValue> {
1015 self.bottom_button_object(button)
1016 .ok()
1017 .and_then(|object| Reflect::get(&object, &property.into()).ok())
1018 }
1019
1020 pub fn hide_keyboard(&self) -> Result<(), JsValue> {
1033 self.call0("hideKeyboard")
1034 }
1035
1036 pub fn read_text_from_clipboard<F>(&self, callback: F) -> Result<(), JsValue>
1051 where
1052 F: 'static + Fn(String)
1053 {
1054 let cb = Closure::<dyn FnMut(JsValue)>::new(move |text: JsValue| {
1055 callback(text.as_string().unwrap_or_default());
1056 });
1057 let f = Reflect::get(&self.inner, &"readTextFromClipboard".into())?;
1058 let func = f
1059 .dyn_ref::<Function>()
1060 .ok_or_else(|| JsValue::from_str("readTextFromClipboard is not a function"))?;
1061 func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
1062 cb.forget();
1063 Ok(())
1064 }
1065
1066 pub fn show_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
1071 self.bottom_button_method(button, "show", None)
1072 }
1073
1074 pub fn hide_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
1079 self.bottom_button_method(button, "hide", None)
1080 }
1081
1082 pub fn ready(&self) -> Result<(), JsValue> {
1087 self.call0("ready")
1088 }
1089
1090 pub fn show_back_button(&self) -> Result<(), JsValue> {
1095 self.call_nested0("BackButton", "show")
1096 }
1097
1098 pub fn hide_back_button(&self) -> Result<(), JsValue> {
1103 self.call_nested0("BackButton", "hide")
1104 }
1105
1106 pub fn set_header_color(&self, color: &str) -> Result<(), JsValue> {
1118 self.call1("setHeaderColor", &color.into())
1119 }
1120
1121 pub fn set_background_color(&self, color: &str) -> Result<(), JsValue> {
1133 self.call1("setBackgroundColor", &color.into())
1134 }
1135
1136 pub fn set_bottom_bar_color(&self, color: &str) -> Result<(), JsValue> {
1148 self.call1("setBottomBarColor", &color.into())
1149 }
1150
1151 pub fn set_bottom_button_text(&self, button: BottomButton, text: &str) -> Result<(), JsValue> {
1156 self.bottom_button_method(button, "setText", Some(&text.into()))
1157 }
1158
1159 pub fn set_bottom_button_color(
1171 &self,
1172 button: BottomButton,
1173 color: &str
1174 ) -> Result<(), JsValue> {
1175 self.bottom_button_method(button, "setColor", Some(&color.into()))
1176 }
1177
1178 pub fn set_bottom_button_text_color(
1190 &self,
1191 button: BottomButton,
1192 color: &str
1193 ) -> Result<(), JsValue> {
1194 self.bottom_button_method(button, "setTextColor", Some(&color.into()))
1195 }
1196
1197 pub fn enable_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
1208 self.bottom_button_method(button, "enable", None)
1209 }
1210
1211 pub fn disable_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
1222 self.bottom_button_method(button, "disable", None)
1223 }
1224
1225 pub fn show_bottom_button_progress(
1236 &self,
1237 button: BottomButton,
1238 leave_active: bool
1239 ) -> Result<(), JsValue> {
1240 let leave_active = JsValue::from_bool(leave_active);
1241 self.bottom_button_method(button, "showProgress", Some(&leave_active))
1242 }
1243
1244 pub fn hide_bottom_button_progress(&self, button: BottomButton) -> Result<(), JsValue> {
1255 self.bottom_button_method(button, "hideProgress", None)
1256 }
1257
1258 pub fn is_bottom_button_visible(&self, button: BottomButton) -> bool {
1269 self.bottom_button_property(button, "isVisible")
1270 .and_then(|v| v.as_bool())
1271 .unwrap_or(false)
1272 }
1273
1274 pub fn is_bottom_button_active(&self, button: BottomButton) -> bool {
1285 self.bottom_button_property(button, "isActive")
1286 .and_then(|v| v.as_bool())
1287 .unwrap_or(false)
1288 }
1289
1290 pub fn is_bottom_button_progress_visible(&self, button: BottomButton) -> bool {
1301 self.bottom_button_property(button, "isProgressVisible")
1302 .and_then(|v| v.as_bool())
1303 .unwrap_or(false)
1304 }
1305
1306 pub fn bottom_button_text(&self, button: BottomButton) -> Option<String> {
1317 self.bottom_button_property(button, "text")?.as_string()
1318 }
1319
1320 pub fn bottom_button_text_color(&self, button: BottomButton) -> Option<String> {
1331 self.bottom_button_property(button, "textColor")?
1332 .as_string()
1333 }
1334
1335 pub fn bottom_button_color(&self, button: BottomButton) -> Option<String> {
1346 self.bottom_button_property(button, "color")?.as_string()
1347 }
1348
1349 pub fn bottom_button_has_shine_effect(&self, button: BottomButton) -> bool {
1360 self.bottom_button_property(button, "hasShineEffect")
1361 .and_then(|v| v.as_bool())
1362 .unwrap_or(false)
1363 }
1364
1365 pub fn set_bottom_button_params(
1380 &self,
1381 button: BottomButton,
1382 params: &BottomButtonParams<'_>
1383 ) -> Result<(), JsValue> {
1384 let value = to_value(params).map_err(|err| JsValue::from_str(&err.to_string()))?;
1385 self.bottom_button_method(button, "setParams", Some(&value))
1386 }
1387
1388 pub fn set_secondary_button_params(
1405 &self,
1406 params: &SecondaryButtonParams<'_>
1407 ) -> Result<(), JsValue> {
1408 let value = to_value(params).map_err(|err| JsValue::from_str(&err.to_string()))?;
1409 self.bottom_button_method(BottomButton::Secondary, "setParams", Some(&value))
1410 }
1411
1412 pub fn secondary_button_position(&self) -> Option<SecondaryButtonPosition> {
1423 self.bottom_button_property(BottomButton::Secondary, "position")
1424 .and_then(SecondaryButtonPosition::from_js_value)
1425 }
1426
1427 pub fn set_bottom_button_callback<F>(
1434 &self,
1435 button: BottomButton,
1436 callback: F
1437 ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1438 where
1439 F: 'static + Fn()
1440 {
1441 let btn_val = Reflect::get(&self.inner, &button.js_name().into())?;
1442 let btn = btn_val.dyn_into::<Object>()?;
1443 let cb = Closure::<dyn FnMut()>::new(callback);
1444 let f = Reflect::get(&btn, &"onClick".into())?;
1445 let func = f
1446 .dyn_ref::<Function>()
1447 .ok_or_else(|| JsValue::from_str("onClick is not a function"))?;
1448 func.call1(&btn, cb.as_ref().unchecked_ref())?;
1449 Ok(EventHandle::new(btn, "offClick", None, cb))
1450 }
1451
1452 pub fn remove_bottom_button_callback(
1457 &self,
1458 handle: EventHandle<dyn FnMut()>
1459 ) -> Result<(), JsValue> {
1460 handle.unregister()
1461 }
1462
1463 pub fn show_main_button(&self) -> Result<(), JsValue> {
1466 self.show_bottom_button(BottomButton::Main)
1467 }
1468
1469 pub fn show_secondary_button(&self) -> Result<(), JsValue> {
1471 self.show_bottom_button(BottomButton::Secondary)
1472 }
1473
1474 pub fn hide_main_button(&self) -> Result<(), JsValue> {
1477 self.hide_bottom_button(BottomButton::Main)
1478 }
1479
1480 pub fn hide_secondary_button(&self) -> Result<(), JsValue> {
1482 self.hide_bottom_button(BottomButton::Secondary)
1483 }
1484
1485 pub fn set_main_button_text(&self, text: &str) -> Result<(), JsValue> {
1488 self.set_bottom_button_text(BottomButton::Main, text)
1489 }
1490
1491 pub fn set_secondary_button_text(&self, text: &str) -> Result<(), JsValue> {
1493 self.set_bottom_button_text(BottomButton::Secondary, text)
1494 }
1495
1496 pub fn set_main_button_color(&self, color: &str) -> Result<(), JsValue> {
1499 self.set_bottom_button_color(BottomButton::Main, color)
1500 }
1501
1502 pub fn set_secondary_button_color(&self, color: &str) -> Result<(), JsValue> {
1504 self.set_bottom_button_color(BottomButton::Secondary, color)
1505 }
1506
1507 pub fn set_main_button_text_color(&self, color: &str) -> Result<(), JsValue> {
1510 self.set_bottom_button_text_color(BottomButton::Main, color)
1511 }
1512
1513 pub fn set_secondary_button_text_color(&self, color: &str) -> Result<(), JsValue> {
1515 self.set_bottom_button_text_color(BottomButton::Secondary, color)
1516 }
1517
1518 pub fn enable_main_button(&self) -> Result<(), JsValue> {
1529 self.enable_bottom_button(BottomButton::Main)
1530 }
1531
1532 pub fn enable_secondary_button(&self) -> Result<(), JsValue> {
1543 self.enable_bottom_button(BottomButton::Secondary)
1544 }
1545
1546 pub fn disable_main_button(&self) -> Result<(), JsValue> {
1557 self.disable_bottom_button(BottomButton::Main)
1558 }
1559
1560 pub fn disable_secondary_button(&self) -> Result<(), JsValue> {
1571 self.disable_bottom_button(BottomButton::Secondary)
1572 }
1573
1574 pub fn show_main_button_progress(&self, leave_active: bool) -> Result<(), JsValue> {
1585 self.show_bottom_button_progress(BottomButton::Main, leave_active)
1586 }
1587
1588 pub fn show_secondary_button_progress(&self, leave_active: bool) -> Result<(), JsValue> {
1599 self.show_bottom_button_progress(BottomButton::Secondary, leave_active)
1600 }
1601
1602 pub fn hide_main_button_progress(&self) -> Result<(), JsValue> {
1613 self.hide_bottom_button_progress(BottomButton::Main)
1614 }
1615
1616 pub fn hide_secondary_button_progress(&self) -> Result<(), JsValue> {
1627 self.hide_bottom_button_progress(BottomButton::Secondary)
1628 }
1629
1630 pub fn set_main_button_params(&self, params: &BottomButtonParams<'_>) -> Result<(), JsValue> {
1633 self.set_bottom_button_params(BottomButton::Main, params)
1634 }
1635
1636 pub fn set_main_button_callback<F>(
1639 &self,
1640 callback: F
1641 ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1642 where
1643 F: 'static + Fn()
1644 {
1645 self.set_bottom_button_callback(BottomButton::Main, callback)
1646 }
1647
1648 pub fn set_secondary_button_callback<F>(
1650 &self,
1651 callback: F
1652 ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1653 where
1654 F: 'static + Fn()
1655 {
1656 self.set_bottom_button_callback(BottomButton::Secondary, callback)
1657 }
1658
1659 pub fn remove_main_button_callback(
1661 &self,
1662 handle: EventHandle<dyn FnMut()>
1663 ) -> Result<(), JsValue> {
1664 self.remove_bottom_button_callback(handle)
1665 }
1666
1667 pub fn remove_secondary_button_callback(
1669 &self,
1670 handle: EventHandle<dyn FnMut()>
1671 ) -> Result<(), JsValue> {
1672 self.remove_bottom_button_callback(handle)
1673 }
1674
1675 pub fn on_event<F>(
1683 &self,
1684 event: &str,
1685 callback: F
1686 ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
1687 where
1688 F: 'static + Fn(JsValue)
1689 {
1690 let cb = Closure::<dyn FnMut(JsValue)>::new(callback);
1691 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1692 let func = f
1693 .dyn_ref::<Function>()
1694 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1695 func.call2(&self.inner, &event.into(), cb.as_ref().unchecked_ref())?;
1696 Ok(EventHandle::new(
1697 self.inner.clone(),
1698 "offEvent",
1699 Some(event.to_owned()),
1700 cb
1701 ))
1702 }
1703
1704 pub fn on_background_event<F>(
1712 &self,
1713 event: BackgroundEvent,
1714 callback: F
1715 ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
1716 where
1717 F: 'static + Fn(JsValue)
1718 {
1719 let cb = Closure::<dyn FnMut(JsValue)>::new(callback);
1720 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1721 let func = f
1722 .dyn_ref::<Function>()
1723 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1724 func.call2(
1725 &self.inner,
1726 &event.as_str().into(),
1727 cb.as_ref().unchecked_ref()
1728 )?;
1729 Ok(EventHandle::new(
1730 self.inner.clone(),
1731 "offEvent",
1732 Some(event.as_str().to_string()),
1733 cb
1734 ))
1735 }
1736
1737 pub fn off_event<T: ?Sized>(&self, handle: EventHandle<T>) -> Result<(), JsValue> {
1742 handle.unregister()
1743 }
1744
1745 fn call_nested0(&self, field: &str, method: &str) -> Result<(), JsValue> {
1747 let obj = Reflect::get(&self.inner, &field.into())?;
1748 let f = Reflect::get(&obj, &method.into())?;
1749 let func = f
1750 .dyn_ref::<Function>()
1751 .ok_or_else(|| JsValue::from_str("not a function"))?;
1752 func.call0(&obj)?;
1753 Ok(())
1754 }
1755
1756 fn call0(&self, method: &str) -> Result<(), JsValue> {
1759 let f = Reflect::get(&self.inner, &method.into())?;
1760 let func = f
1761 .dyn_ref::<Function>()
1762 .ok_or_else(|| JsValue::from_str("not a function"))?;
1763 func.call0(&self.inner)?;
1764 Ok(())
1765 }
1766
1767 fn call1(&self, method: &str, arg: &JsValue) -> Result<(), JsValue> {
1768 let f = Reflect::get(&self.inner, &method.into())?;
1769 let func = f
1770 .dyn_ref::<Function>()
1771 .ok_or_else(|| JsValue::from_str("not a function"))?;
1772 func.call1(&self.inner, arg)?;
1773 Ok(())
1774 }
1775
1776 pub fn viewport_height(&self) -> Option<f64> {
1785 Reflect::get(&self.inner, &"viewportHeight".into())
1786 .ok()?
1787 .as_f64()
1788 }
1789
1790 pub fn viewport_width(&self) -> Option<f64> {
1799 Reflect::get(&self.inner, &"viewportWidth".into())
1800 .ok()?
1801 .as_f64()
1802 }
1803
1804 pub fn viewport_stable_height(&self) -> Option<f64> {
1813 Reflect::get(&self.inner, &"viewportStableHeight".into())
1814 .ok()?
1815 .as_f64()
1816 }
1817
1818 pub fn is_expanded(&self) -> bool {
1819 Reflect::get(&self.inner, &"isExpanded".into())
1820 .ok()
1821 .and_then(|v| v.as_bool())
1822 .unwrap_or(false)
1823 }
1824
1825 pub fn is_active(&self) -> bool {
1836 Reflect::get(&self.inner, &"isActive".into())
1837 .ok()
1838 .and_then(|v| v.as_bool())
1839 .unwrap_or(false)
1840 }
1841
1842 pub fn is_fullscreen(&self) -> bool {
1853 Reflect::get(&self.inner, &"isFullscreen".into())
1854 .ok()
1855 .and_then(|v| v.as_bool())
1856 .unwrap_or(false)
1857 }
1858
1859 pub fn is_orientation_locked(&self) -> bool {
1870 Reflect::get(&self.inner, &"isOrientationLocked".into())
1871 .ok()
1872 .and_then(|v| v.as_bool())
1873 .unwrap_or(false)
1874 }
1875
1876 pub fn is_vertical_swipes_enabled(&self) -> bool {
1887 Reflect::get(&self.inner, &"isVerticalSwipesEnabled".into())
1888 .ok()
1889 .and_then(|v| v.as_bool())
1890 .unwrap_or(false)
1891 }
1892
1893 fn safe_area_from_property(&self, property: &str) -> Option<SafeAreaInset> {
1894 let value = Reflect::get(&self.inner, &property.into()).ok()?;
1895 SafeAreaInset::from_js(value)
1896 }
1897
1898 pub fn safe_area_inset(&self) -> Option<SafeAreaInset> {
1909 self.safe_area_from_property("safeAreaInset")
1910 }
1911
1912 pub fn content_safe_area_inset(&self) -> Option<SafeAreaInset> {
1923 self.safe_area_from_property("contentSafeAreaInset")
1924 }
1925
1926 pub fn expand_viewport(&self) -> Result<(), JsValue> {
1931 self.call0("expand")
1932 }
1933
1934 pub fn on_theme_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
1942 where
1943 F: 'static + Fn()
1944 {
1945 let cb = Closure::<dyn FnMut()>::new(callback);
1946 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1947 let func = f
1948 .dyn_ref::<Function>()
1949 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1950 func.call2(
1951 &self.inner,
1952 &"themeChanged".into(),
1953 cb.as_ref().unchecked_ref()
1954 )?;
1955 Ok(EventHandle::new(
1956 self.inner.clone(),
1957 "offEvent",
1958 Some("themeChanged".to_string()),
1959 cb
1960 ))
1961 }
1962
1963 pub fn on_safe_area_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
1971 where
1972 F: 'static + Fn()
1973 {
1974 let cb = Closure::<dyn FnMut()>::new(callback);
1975 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1976 let func = f
1977 .dyn_ref::<Function>()
1978 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1979 func.call2(
1980 &self.inner,
1981 &"safeAreaChanged".into(),
1982 cb.as_ref().unchecked_ref()
1983 )?;
1984 Ok(EventHandle::new(
1985 self.inner.clone(),
1986 "offEvent",
1987 Some("safeAreaChanged".to_string()),
1988 cb
1989 ))
1990 }
1991
1992 pub fn on_content_safe_area_changed<F>(
2000 &self,
2001 callback: F
2002 ) -> Result<EventHandle<dyn FnMut()>, JsValue>
2003 where
2004 F: 'static + Fn()
2005 {
2006 let cb = Closure::<dyn FnMut()>::new(callback);
2007 let f = Reflect::get(&self.inner, &"onEvent".into())?;
2008 let func = f
2009 .dyn_ref::<Function>()
2010 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
2011 func.call2(
2012 &self.inner,
2013 &"contentSafeAreaChanged".into(),
2014 cb.as_ref().unchecked_ref()
2015 )?;
2016 Ok(EventHandle::new(
2017 self.inner.clone(),
2018 "offEvent",
2019 Some("contentSafeAreaChanged".to_string()),
2020 cb
2021 ))
2022 }
2023
2024 pub fn on_viewport_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
2032 where
2033 F: 'static + Fn()
2034 {
2035 let cb = Closure::<dyn FnMut()>::new(callback);
2036 let f = Reflect::get(&self.inner, &"onEvent".into())?;
2037 let func = f
2038 .dyn_ref::<Function>()
2039 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
2040 func.call2(
2041 &self.inner,
2042 &"viewportChanged".into(),
2043 cb.as_ref().unchecked_ref()
2044 )?;
2045 Ok(EventHandle::new(
2046 self.inner.clone(),
2047 "offEvent",
2048 Some("viewportChanged".to_string()),
2049 cb
2050 ))
2051 }
2052
2053 pub fn on_clipboard_text_received<F>(
2061 &self,
2062 callback: F
2063 ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
2064 where
2065 F: 'static + Fn(String)
2066 {
2067 let cb = Closure::<dyn FnMut(JsValue)>::new(move |text: JsValue| {
2068 callback(text.as_string().unwrap_or_default());
2069 });
2070 let f = Reflect::get(&self.inner, &"onEvent".into())?;
2071 let func = f
2072 .dyn_ref::<Function>()
2073 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
2074 func.call2(
2075 &self.inner,
2076 &"clipboardTextReceived".into(),
2077 cb.as_ref().unchecked_ref()
2078 )?;
2079 Ok(EventHandle::new(
2080 self.inner.clone(),
2081 "offEvent",
2082 Some("clipboardTextReceived".to_string()),
2083 cb
2084 ))
2085 }
2086
2087 pub fn on_invoice_closed<F>(
2107 &self,
2108 callback: F
2109 ) -> Result<EventHandle<dyn FnMut(String)>, JsValue>
2110 where
2111 F: 'static + Fn(String)
2112 {
2113 let cb = Closure::<dyn FnMut(String)>::new(callback);
2114 let f = Reflect::get(&self.inner, &"onEvent".into())?;
2115 let func = f
2116 .dyn_ref::<Function>()
2117 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
2118 func.call2(
2119 &self.inner,
2120 &"invoiceClosed".into(),
2121 cb.as_ref().unchecked_ref()
2122 )?;
2123 Ok(EventHandle::new(
2124 self.inner.clone(),
2125 "offEvent",
2126 Some("invoiceClosed".to_string()),
2127 cb
2128 ))
2129 }
2130
2131 pub fn set_back_button_callback<F>(
2147 &self,
2148 callback: F
2149 ) -> Result<EventHandle<dyn FnMut()>, JsValue>
2150 where
2151 F: 'static + Fn()
2152 {
2153 let back_button_val = Reflect::get(&self.inner, &"BackButton".into())?;
2154 let back_button = back_button_val.dyn_into::<Object>()?;
2155 let cb = Closure::<dyn FnMut()>::new(callback);
2156 let f = Reflect::get(&back_button, &"onClick".into())?;
2157 let func = f
2158 .dyn_ref::<Function>()
2159 .ok_or_else(|| JsValue::from_str("onClick is not a function"))?;
2160 func.call1(&back_button, cb.as_ref().unchecked_ref())?;
2161 Ok(EventHandle::new(back_button, "offClick", None, cb))
2162 }
2163
2164 pub fn remove_back_button_callback(
2169 &self,
2170 handle: EventHandle<dyn FnMut()>
2171 ) -> Result<(), JsValue> {
2172 handle.unregister()
2173 }
2174 pub fn is_back_button_visible(&self) -> bool {
2183 Reflect::get(&self.inner, &"BackButton".into())
2184 .ok()
2185 .and_then(|bb| Reflect::get(&bb, &"isVisible".into()).ok())
2186 .and_then(|v| v.as_bool())
2187 .unwrap_or(false)
2188 }
2189}
2190
2191#[cfg(test)]
2192mod tests {
2193 use std::{
2194 cell::{Cell, RefCell},
2195 rc::Rc
2196 };
2197
2198 use js_sys::{Function, Object, Reflect};
2199 use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
2200 use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
2201 use web_sys::window;
2202
2203 use super::*;
2204
2205 wasm_bindgen_test_configure!(run_in_browser);
2206
2207 #[allow(dead_code)]
2208 fn setup_webapp() -> Object {
2209 let win = window().unwrap();
2210 let telegram = Object::new();
2211 let webapp = Object::new();
2212 let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
2213 let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
2214 webapp
2215 }
2216
2217 #[wasm_bindgen_test]
2218 #[allow(dead_code, clippy::unused_unit)]
2219 fn hide_keyboard_calls_js() {
2220 let webapp = setup_webapp();
2221 let called = Rc::new(Cell::new(false));
2222 let called_clone = Rc::clone(&called);
2223
2224 let hide_cb = Closure::<dyn FnMut()>::new(move || {
2225 called_clone.set(true);
2226 });
2227 let _ = Reflect::set(
2228 &webapp,
2229 &"hideKeyboard".into(),
2230 hide_cb.as_ref().unchecked_ref()
2231 );
2232 hide_cb.forget();
2233
2234 let app = TelegramWebApp::instance().unwrap();
2235 app.hide_keyboard().unwrap();
2236 assert!(called.get());
2237 }
2238
2239 #[wasm_bindgen_test]
2240 #[allow(dead_code, clippy::unused_unit)]
2241 fn hide_main_button_calls_js() {
2242 let webapp = setup_webapp();
2243 let main_button = Object::new();
2244 let called = Rc::new(Cell::new(false));
2245 let called_clone = Rc::clone(&called);
2246
2247 let hide_cb = Closure::<dyn FnMut()>::new(move || {
2248 called_clone.set(true);
2249 });
2250 let _ = Reflect::set(
2251 &main_button,
2252 &"hide".into(),
2253 hide_cb.as_ref().unchecked_ref()
2254 );
2255 hide_cb.forget();
2256
2257 let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
2258
2259 let app = TelegramWebApp::instance().unwrap();
2260 app.hide_bottom_button(BottomButton::Main).unwrap();
2261 assert!(called.get());
2262 }
2263
2264 #[wasm_bindgen_test]
2265 #[allow(dead_code, clippy::unused_unit)]
2266 fn hide_secondary_button_calls_js() {
2267 let webapp = setup_webapp();
2268 let secondary_button = Object::new();
2269 let called = Rc::new(Cell::new(false));
2270 let called_clone = Rc::clone(&called);
2271
2272 let hide_cb = Closure::<dyn FnMut()>::new(move || {
2273 called_clone.set(true);
2274 });
2275 let _ = Reflect::set(
2276 &secondary_button,
2277 &"hide".into(),
2278 hide_cb.as_ref().unchecked_ref()
2279 );
2280 hide_cb.forget();
2281
2282 let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
2283
2284 let app = TelegramWebApp::instance().unwrap();
2285 app.hide_bottom_button(BottomButton::Secondary).unwrap();
2286 assert!(called.get());
2287 }
2288
2289 #[wasm_bindgen_test]
2290 #[allow(dead_code, clippy::unused_unit)]
2291 fn set_bottom_button_color_calls_js() {
2292 let webapp = setup_webapp();
2293 let main_button = Object::new();
2294 let received = Rc::new(RefCell::new(None));
2295 let rc_clone = Rc::clone(&received);
2296
2297 let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2298 *rc_clone.borrow_mut() = v.as_string();
2299 });
2300 let _ = Reflect::set(
2301 &main_button,
2302 &"setColor".into(),
2303 set_color_cb.as_ref().unchecked_ref()
2304 );
2305 set_color_cb.forget();
2306
2307 let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
2308
2309 let app = TelegramWebApp::instance().unwrap();
2310 app.set_bottom_button_color(BottomButton::Main, "#00ff00")
2311 .unwrap();
2312 assert_eq!(received.borrow().as_deref(), Some("#00ff00"));
2313 }
2314
2315 #[wasm_bindgen_test]
2316 #[allow(dead_code, clippy::unused_unit)]
2317 fn set_secondary_button_color_calls_js() {
2318 let webapp = setup_webapp();
2319 let secondary_button = Object::new();
2320 let received = Rc::new(RefCell::new(None));
2321 let rc_clone = Rc::clone(&received);
2322
2323 let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2324 *rc_clone.borrow_mut() = v.as_string();
2325 });
2326 let _ = Reflect::set(
2327 &secondary_button,
2328 &"setColor".into(),
2329 set_color_cb.as_ref().unchecked_ref()
2330 );
2331 set_color_cb.forget();
2332
2333 let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
2334
2335 let app = TelegramWebApp::instance().unwrap();
2336 app.set_bottom_button_color(BottomButton::Secondary, "#00ff00")
2337 .unwrap();
2338 assert_eq!(received.borrow().as_deref(), Some("#00ff00"));
2339 }
2340
2341 #[wasm_bindgen_test]
2342 #[allow(dead_code, clippy::unused_unit)]
2343 fn set_bottom_button_text_color_calls_js() {
2344 let webapp = setup_webapp();
2345 let main_button = Object::new();
2346 let received = Rc::new(RefCell::new(None));
2347 let rc_clone = Rc::clone(&received);
2348
2349 let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2350 *rc_clone.borrow_mut() = v.as_string();
2351 });
2352 let _ = Reflect::set(
2353 &main_button,
2354 &"setTextColor".into(),
2355 set_color_cb.as_ref().unchecked_ref()
2356 );
2357 set_color_cb.forget();
2358
2359 let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
2360
2361 let app = TelegramWebApp::instance().unwrap();
2362 app.set_bottom_button_text_color(BottomButton::Main, "#112233")
2363 .unwrap();
2364 assert_eq!(received.borrow().as_deref(), Some("#112233"));
2365 }
2366
2367 #[wasm_bindgen_test]
2368 #[allow(dead_code, clippy::unused_unit)]
2369 fn set_secondary_button_text_color_calls_js() {
2370 let webapp = setup_webapp();
2371 let secondary_button = Object::new();
2372 let received = Rc::new(RefCell::new(None));
2373 let rc_clone = Rc::clone(&received);
2374
2375 let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2376 *rc_clone.borrow_mut() = v.as_string();
2377 });
2378 let _ = Reflect::set(
2379 &secondary_button,
2380 &"setTextColor".into(),
2381 set_color_cb.as_ref().unchecked_ref()
2382 );
2383 set_color_cb.forget();
2384
2385 let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
2386
2387 let app = TelegramWebApp::instance().unwrap();
2388 app.set_bottom_button_text_color(BottomButton::Secondary, "#112233")
2389 .unwrap();
2390 assert_eq!(received.borrow().as_deref(), Some("#112233"));
2391 }
2392
2393 #[wasm_bindgen_test]
2394 #[allow(dead_code, clippy::unused_unit)]
2395 fn enable_bottom_button_calls_js() {
2396 let webapp = setup_webapp();
2397 let button = Object::new();
2398 let called = Rc::new(Cell::new(false));
2399 let called_clone = Rc::clone(&called);
2400
2401 let enable_cb = Closure::<dyn FnMut()>::new(move || {
2402 called_clone.set(true);
2403 });
2404 let _ = Reflect::set(
2405 &button,
2406 &"enable".into(),
2407 enable_cb.as_ref().unchecked_ref()
2408 );
2409 enable_cb.forget();
2410
2411 let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
2412
2413 let app = TelegramWebApp::instance().unwrap();
2414 app.enable_bottom_button(BottomButton::Main).unwrap();
2415 assert!(called.get());
2416 }
2417
2418 #[wasm_bindgen_test]
2419 #[allow(dead_code, clippy::unused_unit)]
2420 fn show_bottom_button_progress_passes_flag() {
2421 let webapp = setup_webapp();
2422 let button = Object::new();
2423 let received = Rc::new(RefCell::new(None));
2424 let rc_clone = Rc::clone(&received);
2425
2426 let cb = Closure::<dyn FnMut(JsValue)>::new(move |arg: JsValue| {
2427 *rc_clone.borrow_mut() = arg.as_bool();
2428 });
2429 let _ = Reflect::set(&button, &"showProgress".into(), cb.as_ref().unchecked_ref());
2430 cb.forget();
2431
2432 let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
2433
2434 let app = TelegramWebApp::instance().unwrap();
2435 app.show_bottom_button_progress(BottomButton::Main, true)
2436 .unwrap();
2437 assert_eq!(*received.borrow(), Some(true));
2438 }
2439
2440 #[wasm_bindgen_test]
2441 #[allow(dead_code, clippy::unused_unit)]
2442 fn set_bottom_button_params_serializes() {
2443 let webapp = setup_webapp();
2444 let button = Object::new();
2445 let received = Rc::new(RefCell::new(Object::new()));
2446 let rc_clone = Rc::clone(&received);
2447
2448 let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
2449 let obj = value.dyn_into::<Object>().expect("object");
2450 rc_clone.replace(obj);
2451 });
2452 let _ = Reflect::set(&button, &"setParams".into(), cb.as_ref().unchecked_ref());
2453 cb.forget();
2454
2455 let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
2456
2457 let app = TelegramWebApp::instance().unwrap();
2458 let params = BottomButtonParams {
2459 text: Some("Send"),
2460 color: Some("#ffffff"),
2461 text_color: Some("#000000"),
2462 is_active: Some(true),
2463 is_visible: Some(true),
2464 has_shine_effect: Some(false)
2465 };
2466 app.set_bottom_button_params(BottomButton::Main, ¶ms)
2467 .unwrap();
2468
2469 let stored = received.borrow();
2470 assert_eq!(
2471 Reflect::get(&stored, &"text".into()).unwrap().as_string(),
2472 Some("Send".to_string())
2473 );
2474 assert_eq!(
2475 Reflect::get(&stored, &"color".into()).unwrap().as_string(),
2476 Some("#ffffff".to_string())
2477 );
2478 assert_eq!(
2479 Reflect::get(&stored, &"text_color".into())
2480 .unwrap()
2481 .as_string(),
2482 Some("#000000".to_string())
2483 );
2484 }
2485
2486 #[wasm_bindgen_test]
2487 #[allow(dead_code, clippy::unused_unit)]
2488 fn set_secondary_button_params_serializes_position() {
2489 let webapp = setup_webapp();
2490 let button = Object::new();
2491 let received = Rc::new(RefCell::new(Object::new()));
2492 let rc_clone = Rc::clone(&received);
2493
2494 let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
2495 let obj = value.dyn_into::<Object>().expect("object");
2496 rc_clone.replace(obj);
2497 });
2498 let _ = Reflect::set(&button, &"setParams".into(), cb.as_ref().unchecked_ref());
2499 cb.forget();
2500
2501 let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &button);
2502
2503 let app = TelegramWebApp::instance().unwrap();
2504 let params = SecondaryButtonParams {
2505 common: BottomButtonParams {
2506 text: Some("Next"),
2507 ..Default::default()
2508 },
2509 position: Some(SecondaryButtonPosition::Left)
2510 };
2511 app.set_secondary_button_params(¶ms).unwrap();
2512
2513 let stored = received.borrow();
2514 assert_eq!(
2515 Reflect::get(&stored, &"text".into()).unwrap().as_string(),
2516 Some("Next".to_string())
2517 );
2518 assert_eq!(
2519 Reflect::get(&stored, &"position".into())
2520 .unwrap()
2521 .as_string(),
2522 Some("left".to_string())
2523 );
2524 }
2525
2526 #[wasm_bindgen_test]
2527 #[allow(dead_code, clippy::unused_unit)]
2528 fn bottom_button_getters_return_values() {
2529 let webapp = setup_webapp();
2530 let button = Object::new();
2531 let _ = Reflect::set(&button, &"text".into(), &"Label".into());
2532 let _ = Reflect::set(&button, &"textColor".into(), &"#111111".into());
2533 let _ = Reflect::set(&button, &"color".into(), &"#222222".into());
2534 let _ = Reflect::set(&button, &"isVisible".into(), &JsValue::TRUE);
2535 let _ = Reflect::set(&button, &"isActive".into(), &JsValue::TRUE);
2536 let _ = Reflect::set(&button, &"isProgressVisible".into(), &JsValue::FALSE);
2537 let _ = Reflect::set(&button, &"hasShineEffect".into(), &JsValue::TRUE);
2538
2539 let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
2540
2541 let app = TelegramWebApp::instance().unwrap();
2542 assert_eq!(
2543 app.bottom_button_text(BottomButton::Main),
2544 Some("Label".into())
2545 );
2546 assert_eq!(
2547 app.bottom_button_text_color(BottomButton::Main),
2548 Some("#111111".into())
2549 );
2550 assert_eq!(
2551 app.bottom_button_color(BottomButton::Main),
2552 Some("#222222".into())
2553 );
2554 assert!(app.is_bottom_button_visible(BottomButton::Main));
2555 assert!(app.is_bottom_button_active(BottomButton::Main));
2556 assert!(!app.is_bottom_button_progress_visible(BottomButton::Main));
2557 assert!(app.bottom_button_has_shine_effect(BottomButton::Main));
2558 }
2559
2560 #[wasm_bindgen_test]
2561 #[allow(dead_code, clippy::unused_unit)]
2562 fn secondary_button_position_is_parsed() {
2563 let webapp = setup_webapp();
2564 let button = Object::new();
2565 let _ = Reflect::set(&button, &"position".into(), &"right".into());
2566 let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &button);
2567
2568 let app = TelegramWebApp::instance().unwrap();
2569 assert_eq!(
2570 app.secondary_button_position(),
2571 Some(SecondaryButtonPosition::Right)
2572 );
2573 }
2574
2575 #[wasm_bindgen_test]
2576 #[allow(dead_code, clippy::unused_unit)]
2577 fn set_header_color_calls_js() {
2578 let webapp = setup_webapp();
2579 let received = Rc::new(RefCell::new(None));
2580 let rc_clone = Rc::clone(&received);
2581
2582 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2583 *rc_clone.borrow_mut() = v.as_string();
2584 });
2585 let _ = Reflect::set(
2586 &webapp,
2587 &"setHeaderColor".into(),
2588 cb.as_ref().unchecked_ref()
2589 );
2590 cb.forget();
2591
2592 let app = TelegramWebApp::instance().unwrap();
2593 app.set_header_color("#abcdef").unwrap();
2594 assert_eq!(received.borrow().as_deref(), Some("#abcdef"));
2595 }
2596
2597 #[wasm_bindgen_test]
2598 #[allow(dead_code, clippy::unused_unit)]
2599 fn set_background_color_calls_js() {
2600 let webapp = setup_webapp();
2601 let received = Rc::new(RefCell::new(None));
2602 let rc_clone = Rc::clone(&received);
2603
2604 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2605 *rc_clone.borrow_mut() = v.as_string();
2606 });
2607 let _ = Reflect::set(
2608 &webapp,
2609 &"setBackgroundColor".into(),
2610 cb.as_ref().unchecked_ref()
2611 );
2612 cb.forget();
2613
2614 let app = TelegramWebApp::instance().unwrap();
2615 app.set_background_color("#123456").unwrap();
2616 assert_eq!(received.borrow().as_deref(), Some("#123456"));
2617 }
2618
2619 #[wasm_bindgen_test]
2620 #[allow(dead_code, clippy::unused_unit)]
2621 fn set_bottom_bar_color_calls_js() {
2622 let webapp = setup_webapp();
2623 let received = Rc::new(RefCell::new(None));
2624 let rc_clone = Rc::clone(&received);
2625
2626 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2627 *rc_clone.borrow_mut() = v.as_string();
2628 });
2629 let _ = Reflect::set(
2630 &webapp,
2631 &"setBottomBarColor".into(),
2632 cb.as_ref().unchecked_ref()
2633 );
2634 cb.forget();
2635
2636 let app = TelegramWebApp::instance().unwrap();
2637 app.set_bottom_bar_color("#654321").unwrap();
2638 assert_eq!(received.borrow().as_deref(), Some("#654321"));
2639 }
2640
2641 #[wasm_bindgen_test]
2642 #[allow(dead_code, clippy::unused_unit)]
2643 fn viewport_dimensions() {
2644 let webapp = setup_webapp();
2645 let _ = Reflect::set(&webapp, &"viewportWidth".into(), &JsValue::from_f64(320.0));
2646 let _ = Reflect::set(
2647 &webapp,
2648 &"viewportStableHeight".into(),
2649 &JsValue::from_f64(480.0)
2650 );
2651 let app = TelegramWebApp::instance().unwrap();
2652 assert_eq!(app.viewport_width(), Some(320.0));
2653 assert_eq!(app.viewport_stable_height(), Some(480.0));
2654 }
2655
2656 #[wasm_bindgen_test]
2657 #[allow(dead_code, clippy::unused_unit)]
2658 fn version_check_invokes_js() {
2659 let webapp = setup_webapp();
2660 let cb = Function::new_with_args("v", "return v === '9.0';");
2661 let _ = Reflect::set(&webapp, &"isVersionAtLeast".into(), &cb);
2662
2663 let app = TelegramWebApp::instance().unwrap();
2664 assert!(app.is_version_at_least("9.0").unwrap());
2665 assert!(!app.is_version_at_least("9.1").unwrap());
2666 }
2667
2668 #[wasm_bindgen_test]
2669 #[allow(dead_code, clippy::unused_unit)]
2670 fn safe_area_insets_are_parsed() {
2671 let webapp = setup_webapp();
2672 let safe_area = Object::new();
2673 let _ = Reflect::set(&safe_area, &"top".into(), &JsValue::from_f64(1.0));
2674 let _ = Reflect::set(&safe_area, &"bottom".into(), &JsValue::from_f64(2.0));
2675 let _ = Reflect::set(&safe_area, &"left".into(), &JsValue::from_f64(3.0));
2676 let _ = Reflect::set(&safe_area, &"right".into(), &JsValue::from_f64(4.0));
2677 let _ = Reflect::set(&webapp, &"safeAreaInset".into(), &safe_area);
2678
2679 let content_safe = Object::new();
2680 let _ = Reflect::set(&content_safe, &"top".into(), &JsValue::from_f64(5.0));
2681 let _ = Reflect::set(&content_safe, &"bottom".into(), &JsValue::from_f64(6.0));
2682 let _ = Reflect::set(&content_safe, &"left".into(), &JsValue::from_f64(7.0));
2683 let _ = Reflect::set(&content_safe, &"right".into(), &JsValue::from_f64(8.0));
2684 let _ = Reflect::set(&webapp, &"contentSafeAreaInset".into(), &content_safe);
2685
2686 let app = TelegramWebApp::instance().unwrap();
2687 let inset = app.safe_area_inset().expect("safe area");
2688 assert_eq!(inset.top, 1.0);
2689 assert_eq!(inset.bottom, 2.0);
2690 assert_eq!(inset.left, 3.0);
2691 assert_eq!(inset.right, 4.0);
2692
2693 let content = app.content_safe_area_inset().expect("content area");
2694 assert_eq!(content.top, 5.0);
2695 }
2696
2697 #[wasm_bindgen_test]
2698 #[allow(dead_code, clippy::unused_unit)]
2699 fn activity_flags_are_reported() {
2700 let webapp = setup_webapp();
2701 let _ = Reflect::set(&webapp, &"isActive".into(), &JsValue::TRUE);
2702 let _ = Reflect::set(&webapp, &"isFullscreen".into(), &JsValue::TRUE);
2703 let _ = Reflect::set(&webapp, &"isOrientationLocked".into(), &JsValue::FALSE);
2704 let _ = Reflect::set(&webapp, &"isVerticalSwipesEnabled".into(), &JsValue::TRUE);
2705
2706 let app = TelegramWebApp::instance().unwrap();
2707 assert!(app.is_active());
2708 assert!(app.is_fullscreen());
2709 assert!(!app.is_orientation_locked());
2710 assert!(app.is_vertical_swipes_enabled());
2711 }
2712
2713 #[wasm_bindgen_test]
2714 #[allow(dead_code, clippy::unused_unit)]
2715 fn back_button_visibility_and_callback() {
2716 let webapp = setup_webapp();
2717 let back_button = Object::new();
2718 let _ = Reflect::set(&webapp, &"BackButton".into(), &back_button);
2719 let _ = Reflect::set(&back_button, &"isVisible".into(), &JsValue::TRUE);
2720
2721 let on_click = Function::new_with_args("cb", "this.cb = cb;");
2722 let off_click = Function::new_with_args("", "delete this.cb;");
2723 let _ = Reflect::set(&back_button, &"onClick".into(), &on_click);
2724 let _ = Reflect::set(&back_button, &"offClick".into(), &off_click);
2725
2726 let called = Rc::new(Cell::new(false));
2727 let called_clone = Rc::clone(&called);
2728
2729 let app = TelegramWebApp::instance().unwrap();
2730 assert!(app.is_back_button_visible());
2731 let handle = app
2732 .set_back_button_callback(move || {
2733 called_clone.set(true);
2734 })
2735 .unwrap();
2736
2737 let stored = Reflect::has(&back_button, &"cb".into()).unwrap();
2738 assert!(stored);
2739
2740 let cb_fn = Reflect::get(&back_button, &"cb".into())
2741 .unwrap()
2742 .dyn_into::<Function>()
2743 .unwrap();
2744 let _ = cb_fn.call0(&JsValue::NULL);
2745 assert!(called.get());
2746
2747 app.remove_back_button_callback(handle).unwrap();
2748 let stored_after = Reflect::has(&back_button, &"cb".into()).unwrap();
2749 assert!(!stored_after);
2750 }
2751
2752 #[wasm_bindgen_test]
2753 #[allow(dead_code, clippy::unused_unit)]
2754 fn bottom_button_callback_register_and_remove() {
2755 let webapp = setup_webapp();
2756 let main_button = Object::new();
2757 let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
2758
2759 let on_click = Function::new_with_args("cb", "this.cb = cb;");
2760 let off_click = Function::new_with_args("", "delete this.cb;");
2761 let _ = Reflect::set(&main_button, &"onClick".into(), &on_click);
2762 let _ = Reflect::set(&main_button, &"offClick".into(), &off_click);
2763
2764 let called = Rc::new(Cell::new(false));
2765 let called_clone = Rc::clone(&called);
2766
2767 let app = TelegramWebApp::instance().unwrap();
2768 let handle = app
2769 .set_bottom_button_callback(BottomButton::Main, move || {
2770 called_clone.set(true);
2771 })
2772 .unwrap();
2773
2774 let stored = Reflect::has(&main_button, &"cb".into()).unwrap();
2775 assert!(stored);
2776
2777 let cb_fn = Reflect::get(&main_button, &"cb".into())
2778 .unwrap()
2779 .dyn_into::<Function>()
2780 .unwrap();
2781 let _ = cb_fn.call0(&JsValue::NULL);
2782 assert!(called.get());
2783
2784 app.remove_bottom_button_callback(handle).unwrap();
2785 let stored_after = Reflect::has(&main_button, &"cb".into()).unwrap();
2786 assert!(!stored_after);
2787 }
2788
2789 #[wasm_bindgen_test]
2790 #[allow(dead_code, clippy::unused_unit)]
2791 fn secondary_button_callback_register_and_remove() {
2792 let webapp = setup_webapp();
2793 let secondary_button = Object::new();
2794 let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
2795
2796 let on_click = Function::new_with_args("cb", "this.cb = cb;");
2797 let off_click = Function::new_with_args("", "delete this.cb;");
2798 let _ = Reflect::set(&secondary_button, &"onClick".into(), &on_click);
2799 let _ = Reflect::set(&secondary_button, &"offClick".into(), &off_click);
2800
2801 let called = Rc::new(Cell::new(false));
2802 let called_clone = Rc::clone(&called);
2803
2804 let app = TelegramWebApp::instance().unwrap();
2805 let handle = app
2806 .set_bottom_button_callback(BottomButton::Secondary, move || {
2807 called_clone.set(true);
2808 })
2809 .unwrap();
2810
2811 let stored = Reflect::has(&secondary_button, &"cb".into()).unwrap();
2812 assert!(stored);
2813
2814 let cb_fn = Reflect::get(&secondary_button, &"cb".into())
2815 .unwrap()
2816 .dyn_into::<Function>()
2817 .unwrap();
2818 let _ = cb_fn.call0(&JsValue::NULL);
2819 assert!(called.get());
2820
2821 app.remove_bottom_button_callback(handle).unwrap();
2822 let stored_after = Reflect::has(&secondary_button, &"cb".into()).unwrap();
2823 assert!(!stored_after);
2824 }
2825
2826 #[wasm_bindgen_test]
2827 #[allow(dead_code, clippy::unused_unit)]
2828 fn on_event_register_and_remove() {
2829 let webapp = setup_webapp();
2830 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2831 let off_event = Function::new_with_args("name", "delete this[name];");
2832 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2833 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2834
2835 let app = TelegramWebApp::instance().unwrap();
2836 let handle = app.on_event("test", |_: JsValue| {}).unwrap();
2837 assert!(Reflect::has(&webapp, &"test".into()).unwrap());
2838 app.off_event(handle).unwrap();
2839 assert!(!Reflect::has(&webapp, &"test".into()).unwrap());
2840 }
2841
2842 #[wasm_bindgen_test]
2843 #[allow(dead_code, clippy::unused_unit)]
2844 fn background_event_register_and_remove() {
2845 let webapp = setup_webapp();
2846 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2847 let off_event = Function::new_with_args("name", "delete this[name];");
2848 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2849 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2850
2851 let app = TelegramWebApp::instance().unwrap();
2852 let handle = app
2853 .on_background_event(BackgroundEvent::MainButtonClicked, |_| {})
2854 .unwrap();
2855 assert!(Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
2856 app.off_event(handle).unwrap();
2857 assert!(!Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
2858 }
2859
2860 #[wasm_bindgen_test]
2861 #[allow(dead_code, clippy::unused_unit)]
2862 fn background_event_delivers_data() {
2863 let webapp = setup_webapp();
2864 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2865 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2866
2867 let app = TelegramWebApp::instance().unwrap();
2868 let received = Rc::new(RefCell::new(String::new()));
2869 let received_clone = Rc::clone(&received);
2870 let _handle = app
2871 .on_background_event(BackgroundEvent::InvoiceClosed, move |v| {
2872 *received_clone.borrow_mut() = v.as_string().unwrap_or_default();
2873 })
2874 .unwrap();
2875
2876 let cb = Reflect::get(&webapp, &"invoiceClosed".into())
2877 .unwrap()
2878 .dyn_into::<Function>()
2879 .unwrap();
2880 let _ = cb.call1(&JsValue::NULL, &JsValue::from_str("paid"));
2881 assert_eq!(received.borrow().as_str(), "paid");
2882 }
2883
2884 #[wasm_bindgen_test]
2885 #[allow(dead_code, clippy::unused_unit)]
2886 fn theme_changed_register_and_remove() {
2887 let webapp = setup_webapp();
2888 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2889 let off_event = Function::new_with_args("name", "delete this[name];");
2890 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2891 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2892
2893 let app = TelegramWebApp::instance().unwrap();
2894 let handle = app.on_theme_changed(|| {}).unwrap();
2895 assert!(Reflect::has(&webapp, &"themeChanged".into()).unwrap());
2896 app.off_event(handle).unwrap();
2897 assert!(!Reflect::has(&webapp, &"themeChanged".into()).unwrap());
2898 }
2899
2900 #[wasm_bindgen_test]
2901 #[allow(dead_code, clippy::unused_unit)]
2902 fn safe_area_changed_register_and_remove() {
2903 let webapp = setup_webapp();
2904 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2905 let off_event = Function::new_with_args("name", "delete this[name];");
2906 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2907 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2908
2909 let app = TelegramWebApp::instance().unwrap();
2910 let handle = app.on_safe_area_changed(|| {}).unwrap();
2911 assert!(Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
2912 app.off_event(handle).unwrap();
2913 assert!(!Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
2914 }
2915
2916 #[wasm_bindgen_test]
2917 #[allow(dead_code, clippy::unused_unit)]
2918 fn content_safe_area_changed_register_and_remove() {
2919 let webapp = setup_webapp();
2920 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2921 let off_event = Function::new_with_args("name", "delete this[name];");
2922 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2923 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2924
2925 let app = TelegramWebApp::instance().unwrap();
2926 let handle = app.on_content_safe_area_changed(|| {}).unwrap();
2927 assert!(Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
2928 app.off_event(handle).unwrap();
2929 assert!(!Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
2930 }
2931
2932 #[wasm_bindgen_test]
2933 #[allow(dead_code, clippy::unused_unit)]
2934 fn viewport_changed_register_and_remove() {
2935 let webapp = setup_webapp();
2936 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2937 let off_event = Function::new_with_args("name", "delete this[name];");
2938 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2939 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2940
2941 let app = TelegramWebApp::instance().unwrap();
2942 let handle = app.on_viewport_changed(|| {}).unwrap();
2943 assert!(Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
2944 app.off_event(handle).unwrap();
2945 assert!(!Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
2946 }
2947
2948 #[wasm_bindgen_test]
2949 #[allow(dead_code, clippy::unused_unit)]
2950 fn clipboard_text_received_register_and_remove() {
2951 let webapp = setup_webapp();
2952 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2953 let off_event = Function::new_with_args("name", "delete this[name];");
2954 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2955 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2956
2957 let app = TelegramWebApp::instance().unwrap();
2958 let handle = app.on_clipboard_text_received(|_| {}).unwrap();
2959 assert!(Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
2960 app.off_event(handle).unwrap();
2961 assert!(!Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
2962 }
2963
2964 #[wasm_bindgen_test]
2965 #[allow(dead_code, clippy::unused_unit)]
2966 fn open_link_and_telegram_link() {
2967 let webapp = setup_webapp();
2968 let open_link = Function::new_with_args("url", "this.open_link = url;");
2969 let open_tg_link = Function::new_with_args("url", "this.open_tg_link = url;");
2970 let _ = Reflect::set(&webapp, &"openLink".into(), &open_link);
2971 let _ = Reflect::set(&webapp, &"openTelegramLink".into(), &open_tg_link);
2972
2973 let app = TelegramWebApp::instance().unwrap();
2974 let url = "https://example.com";
2975 app.open_link(url, None).unwrap();
2976 app.open_telegram_link(url).unwrap();
2977
2978 assert_eq!(
2979 Reflect::get(&webapp, &"open_link".into())
2980 .unwrap()
2981 .as_string()
2982 .as_deref(),
2983 Some(url)
2984 );
2985 assert_eq!(
2986 Reflect::get(&webapp, &"open_tg_link".into())
2987 .unwrap()
2988 .as_string()
2989 .as_deref(),
2990 Some(url)
2991 );
2992 }
2993
2994 #[wasm_bindgen_test]
2995 #[allow(dead_code, clippy::unused_unit)]
2996 fn invoice_closed_register_and_remove() {
2997 let webapp = setup_webapp();
2998 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2999 let off_event = Function::new_with_args("name", "delete this[name];");
3000 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
3001 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
3002
3003 let app = TelegramWebApp::instance().unwrap();
3004 let handle = app.on_invoice_closed(|_| {}).unwrap();
3005 assert!(Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
3006 app.off_event(handle).unwrap();
3007 assert!(!Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
3008 }
3009
3010 #[wasm_bindgen_test]
3011 #[allow(dead_code, clippy::unused_unit)]
3012 fn invoice_closed_invokes_callback() {
3013 let webapp = setup_webapp();
3014 let on_event = Function::new_with_args("name, cb", "this.cb = cb;");
3015 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
3016
3017 let app = TelegramWebApp::instance().unwrap();
3018 let status = Rc::new(RefCell::new(String::new()));
3019 let status_clone = Rc::clone(&status);
3020 app.on_invoice_closed(move |s| {
3021 *status_clone.borrow_mut() = s;
3022 })
3023 .unwrap();
3024
3025 let cb = Reflect::get(&webapp, &"cb".into())
3026 .unwrap()
3027 .dyn_into::<Function>()
3028 .unwrap();
3029 cb.call1(&webapp, &"paid".into()).unwrap();
3030 assert_eq!(status.borrow().as_str(), "paid");
3031 cb.call1(&webapp, &"failed".into()).unwrap();
3032 assert_eq!(status.borrow().as_str(), "failed");
3033 }
3034
3035 #[wasm_bindgen_test]
3036 #[allow(dead_code, clippy::unused_unit)]
3037 fn open_invoice_invokes_callback() {
3038 let webapp = setup_webapp();
3039 let open_invoice = Function::new_with_args("url, cb", "cb('paid');");
3040 let _ = Reflect::set(&webapp, &"openInvoice".into(), &open_invoice);
3041
3042 let app = TelegramWebApp::instance().unwrap();
3043 let status = Rc::new(RefCell::new(String::new()));
3044 let status_clone = Rc::clone(&status);
3045
3046 app.open_invoice("https://invoice", move |s| {
3047 *status_clone.borrow_mut() = s;
3048 })
3049 .unwrap();
3050
3051 assert_eq!(status.borrow().as_str(), "paid");
3052 }
3053
3054 #[wasm_bindgen_test]
3055 #[allow(dead_code, clippy::unused_unit)]
3056 fn switch_inline_query_calls_js() {
3057 let webapp = setup_webapp();
3058 let switch_inline =
3059 Function::new_with_args("query, types", "this.query = query; this.types = types;");
3060 let _ = Reflect::set(&webapp, &"switchInlineQuery".into(), &switch_inline);
3061
3062 let app = TelegramWebApp::instance().unwrap();
3063 let types = JsValue::from_str("users");
3064 app.switch_inline_query("search", Some(&types)).unwrap();
3065
3066 assert_eq!(
3067 Reflect::get(&webapp, &"query".into())
3068 .unwrap()
3069 .as_string()
3070 .as_deref(),
3071 Some("search"),
3072 );
3073 assert_eq!(
3074 Reflect::get(&webapp, &"types".into())
3075 .unwrap()
3076 .as_string()
3077 .as_deref(),
3078 Some("users"),
3079 );
3080 }
3081
3082 #[wasm_bindgen_test]
3083 #[allow(dead_code, clippy::unused_unit)]
3084 fn share_message_calls_js() {
3085 let webapp = setup_webapp();
3086 let share = Function::new_with_args("id, cb", "this.shared_id = id; cb(true);");
3087 let _ = Reflect::set(&webapp, &"shareMessage".into(), &share);
3088
3089 let app = TelegramWebApp::instance().unwrap();
3090 let sent = Rc::new(Cell::new(false));
3091 let sent_clone = Rc::clone(&sent);
3092
3093 app.share_message("123", move |s| {
3094 sent_clone.set(s);
3095 })
3096 .unwrap();
3097
3098 assert_eq!(
3099 Reflect::get(&webapp, &"shared_id".into())
3100 .unwrap()
3101 .as_string()
3102 .as_deref(),
3103 Some("123"),
3104 );
3105 assert!(sent.get());
3106 }
3107
3108 #[wasm_bindgen_test]
3109 #[allow(dead_code, clippy::unused_unit)]
3110 fn share_to_story_calls_js() {
3111 let webapp = setup_webapp();
3112 let share = Function::new_with_args(
3113 "url, params",
3114 "this.story_url = url; this.story_params = params;"
3115 );
3116 let _ = Reflect::set(&webapp, &"shareToStory".into(), &share);
3117
3118 let app = TelegramWebApp::instance().unwrap();
3119 let url = "https://example.com/media";
3120 let params = Object::new();
3121 let _ = Reflect::set(¶ms, &"text".into(), &"hi".into());
3122 app.share_to_story(url, Some(¶ms.into())).unwrap();
3123
3124 assert_eq!(
3125 Reflect::get(&webapp, &"story_url".into())
3126 .unwrap()
3127 .as_string()
3128 .as_deref(),
3129 Some(url),
3130 );
3131 let stored = Reflect::get(&webapp, &"story_params".into()).unwrap();
3132 assert_eq!(
3133 Reflect::get(&stored, &"text".into())
3134 .unwrap()
3135 .as_string()
3136 .as_deref(),
3137 Some("hi"),
3138 );
3139 }
3140
3141 #[wasm_bindgen_test]
3142 #[allow(dead_code, clippy::unused_unit)]
3143 fn share_url_calls_js() {
3144 let webapp = setup_webapp();
3145 let share = Function::new_with_args(
3146 "url, text",
3147 "this.shared_url = url; this.shared_text = text;"
3148 );
3149 let _ = Reflect::set(&webapp, &"shareURL".into(), &share);
3150
3151 let app = TelegramWebApp::instance().unwrap();
3152 let url = "https://example.com";
3153 let text = "check";
3154 app.share_url(url, Some(text)).unwrap();
3155
3156 assert_eq!(
3157 Reflect::get(&webapp, &"shared_url".into())
3158 .unwrap()
3159 .as_string()
3160 .as_deref(),
3161 Some(url),
3162 );
3163 assert_eq!(
3164 Reflect::get(&webapp, &"shared_text".into())
3165 .unwrap()
3166 .as_string()
3167 .as_deref(),
3168 Some(text),
3169 );
3170 }
3171
3172 #[wasm_bindgen_test]
3173 #[allow(dead_code, clippy::unused_unit)]
3174 fn join_voice_chat_calls_js() {
3175 let webapp = setup_webapp();
3176 let join = Function::new_with_args(
3177 "id, hash",
3178 "this.voice_chat_id = id; this.voice_chat_hash = hash;"
3179 );
3180 let _ = Reflect::set(&webapp, &"joinVoiceChat".into(), &join);
3181
3182 let app = TelegramWebApp::instance().unwrap();
3183 app.join_voice_chat("123", Some("hash")).unwrap();
3184
3185 assert_eq!(
3186 Reflect::get(&webapp, &"voice_chat_id".into())
3187 .unwrap()
3188 .as_string()
3189 .as_deref(),
3190 Some("123"),
3191 );
3192 assert_eq!(
3193 Reflect::get(&webapp, &"voice_chat_hash".into())
3194 .unwrap()
3195 .as_string()
3196 .as_deref(),
3197 Some("hash"),
3198 );
3199 }
3200
3201 #[wasm_bindgen_test]
3202 #[allow(dead_code, clippy::unused_unit)]
3203 fn add_to_home_screen_calls_js() {
3204 let webapp = setup_webapp();
3205 let add = Function::new_with_args("", "this.called = true; return true;");
3206 let _ = Reflect::set(&webapp, &"addToHomeScreen".into(), &add);
3207
3208 let app = TelegramWebApp::instance().unwrap();
3209 let shown = app.add_to_home_screen().unwrap();
3210 assert!(shown);
3211 let called = Reflect::get(&webapp, &"called".into())
3212 .unwrap()
3213 .as_bool()
3214 .unwrap_or(false);
3215 assert!(called);
3216 }
3217
3218 #[wasm_bindgen_test]
3219 #[allow(dead_code, clippy::unused_unit)]
3220 fn request_fullscreen_calls_js() {
3221 let webapp = setup_webapp();
3222 let called = Rc::new(Cell::new(false));
3223 let called_clone = Rc::clone(&called);
3224
3225 let cb = Closure::<dyn FnMut()>::new(move || {
3226 called_clone.set(true);
3227 });
3228 let _ = Reflect::set(
3229 &webapp,
3230 &"requestFullscreen".into(),
3231 cb.as_ref().unchecked_ref()
3232 );
3233 cb.forget();
3234
3235 let app = TelegramWebApp::instance().unwrap();
3236 app.request_fullscreen().unwrap();
3237 assert!(called.get());
3238 }
3239
3240 #[wasm_bindgen_test]
3241 #[allow(dead_code, clippy::unused_unit)]
3242 fn exit_fullscreen_calls_js() {
3243 let webapp = setup_webapp();
3244 let called = Rc::new(Cell::new(false));
3245 let called_clone = Rc::clone(&called);
3246
3247 let cb = Closure::<dyn FnMut()>::new(move || {
3248 called_clone.set(true);
3249 });
3250 let _ = Reflect::set(
3251 &webapp,
3252 &"exitFullscreen".into(),
3253 cb.as_ref().unchecked_ref()
3254 );
3255 cb.forget();
3256
3257 let app = TelegramWebApp::instance().unwrap();
3258 app.exit_fullscreen().unwrap();
3259 assert!(called.get());
3260 }
3261
3262 #[wasm_bindgen_test]
3263 #[allow(dead_code, clippy::unused_unit)]
3264 fn check_home_screen_status_invokes_callback() {
3265 let webapp = setup_webapp();
3266 let check = Function::new_with_args("cb", "cb('added');");
3267 let _ = Reflect::set(&webapp, &"checkHomeScreenStatus".into(), &check);
3268
3269 let app = TelegramWebApp::instance().unwrap();
3270 let status = Rc::new(RefCell::new(String::new()));
3271 let status_clone = Rc::clone(&status);
3272
3273 app.check_home_screen_status(move |s| {
3274 *status_clone.borrow_mut() = s;
3275 })
3276 .unwrap();
3277
3278 assert_eq!(status.borrow().as_str(), "added");
3279 }
3280
3281 #[wasm_bindgen_test]
3282 #[allow(dead_code, clippy::unused_unit)]
3283 fn lock_orientation_calls_js() {
3284 let webapp = setup_webapp();
3285 let received = Rc::new(RefCell::new(None));
3286 let rc_clone = Rc::clone(&received);
3287
3288 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
3289 *rc_clone.borrow_mut() = v.as_string();
3290 });
3291 let _ = Reflect::set(
3292 &webapp,
3293 &"lockOrientation".into(),
3294 cb.as_ref().unchecked_ref()
3295 );
3296 cb.forget();
3297
3298 let app = TelegramWebApp::instance().unwrap();
3299 app.lock_orientation("portrait").unwrap();
3300 assert_eq!(received.borrow().as_deref(), Some("portrait"));
3301 }
3302
3303 #[wasm_bindgen_test]
3304 #[allow(dead_code, clippy::unused_unit)]
3305 fn unlock_orientation_calls_js() {
3306 let webapp = setup_webapp();
3307 let called = Rc::new(Cell::new(false));
3308 let called_clone = Rc::clone(&called);
3309
3310 let cb = Closure::<dyn FnMut()>::new(move || {
3311 called_clone.set(true);
3312 });
3313 let _ = Reflect::set(
3314 &webapp,
3315 &"unlockOrientation".into(),
3316 cb.as_ref().unchecked_ref()
3317 );
3318 cb.forget();
3319
3320 let app = TelegramWebApp::instance().unwrap();
3321 app.unlock_orientation().unwrap();
3322 assert!(called.get());
3323 }
3324
3325 #[wasm_bindgen_test]
3326 #[allow(dead_code, clippy::unused_unit)]
3327 fn enable_vertical_swipes_calls_js() {
3328 let webapp = setup_webapp();
3329 let called = Rc::new(Cell::new(false));
3330 let called_clone = Rc::clone(&called);
3331
3332 let cb = Closure::<dyn FnMut()>::new(move || {
3333 called_clone.set(true);
3334 });
3335 let _ = Reflect::set(
3336 &webapp,
3337 &"enableVerticalSwipes".into(),
3338 cb.as_ref().unchecked_ref()
3339 );
3340 cb.forget();
3341
3342 let app = TelegramWebApp::instance().unwrap();
3343 app.enable_vertical_swipes().unwrap();
3344 assert!(called.get());
3345 }
3346
3347 #[wasm_bindgen_test]
3348 #[allow(dead_code, clippy::unused_unit)]
3349 fn disable_vertical_swipes_calls_js() {
3350 let webapp = setup_webapp();
3351 let called = Rc::new(Cell::new(false));
3352 let called_clone = Rc::clone(&called);
3353
3354 let cb = Closure::<dyn FnMut()>::new(move || {
3355 called_clone.set(true);
3356 });
3357 let _ = Reflect::set(
3358 &webapp,
3359 &"disableVerticalSwipes".into(),
3360 cb.as_ref().unchecked_ref()
3361 );
3362 cb.forget();
3363
3364 let app = TelegramWebApp::instance().unwrap();
3365 app.disable_vertical_swipes().unwrap();
3366 assert!(called.get());
3367 }
3368
3369 #[wasm_bindgen_test]
3370 #[allow(dead_code, clippy::unused_unit)]
3371 fn request_write_access_invokes_callback() {
3372 let webapp = setup_webapp();
3373 let request = Function::new_with_args("cb", "cb(true);");
3374 let _ = Reflect::set(&webapp, &"requestWriteAccess".into(), &request);
3375
3376 let app = TelegramWebApp::instance().unwrap();
3377 let granted = Rc::new(Cell::new(false));
3378 let granted_clone = Rc::clone(&granted);
3379
3380 let res = app.request_write_access(move |g| {
3381 granted_clone.set(g);
3382 });
3383 assert!(res.is_ok());
3384
3385 assert!(granted.get());
3386 }
3387
3388 #[wasm_bindgen_test]
3389 #[allow(dead_code, clippy::unused_unit)]
3390 fn download_file_invokes_callback() {
3391 let webapp = setup_webapp();
3392 let received_url = Rc::new(RefCell::new(String::new()));
3393 let received_name = Rc::new(RefCell::new(String::new()));
3394 let url_clone = Rc::clone(&received_url);
3395 let name_clone = Rc::clone(&received_name);
3396
3397 let download = Closure::<dyn FnMut(JsValue, JsValue)>::new(move |params, cb: JsValue| {
3398 let url = Reflect::get(¶ms, &"url".into())
3399 .unwrap()
3400 .as_string()
3401 .unwrap_or_default();
3402 let name = Reflect::get(¶ms, &"file_name".into())
3403 .unwrap()
3404 .as_string()
3405 .unwrap_or_default();
3406 *url_clone.borrow_mut() = url;
3407 *name_clone.borrow_mut() = name;
3408 let func = cb.dyn_ref::<Function>().unwrap();
3409 let _ = func.call1(&JsValue::NULL, &JsValue::from_str("id"));
3410 });
3411 let _ = Reflect::set(
3412 &webapp,
3413 &"downloadFile".into(),
3414 download.as_ref().unchecked_ref()
3415 );
3416 download.forget();
3417
3418 let app = TelegramWebApp::instance().unwrap();
3419 let result = Rc::new(RefCell::new(String::new()));
3420 let result_clone = Rc::clone(&result);
3421 let params = DownloadFileParams {
3422 url: "https://example.com/data.bin",
3423 file_name: Some("data.bin"),
3424 mime_type: None
3425 };
3426 app.download_file(params, move |id| {
3427 *result_clone.borrow_mut() = id;
3428 })
3429 .unwrap();
3430
3431 assert_eq!(
3432 received_url.borrow().as_str(),
3433 "https://example.com/data.bin"
3434 );
3435 assert_eq!(received_name.borrow().as_str(), "data.bin");
3436 assert_eq!(result.borrow().as_str(), "id");
3437 }
3438
3439 #[wasm_bindgen_test]
3440 #[allow(dead_code, clippy::unused_unit)]
3441 fn request_write_access_returns_error_when_missing() {
3442 let _webapp = setup_webapp();
3443 let app = TelegramWebApp::instance().unwrap();
3444 let res = app.request_write_access(|_| {});
3445 assert!(res.is_err());
3446 }
3447 #[wasm_bindgen_test]
3448 #[allow(dead_code, clippy::unused_unit)]
3449 fn request_emoji_status_access_invokes_callback() {
3450 let webapp = setup_webapp();
3451 let request = Function::new_with_args("cb", "cb(false);");
3452 let _ = Reflect::set(&webapp, &"requestEmojiStatusAccess".into(), &request);
3453
3454 let app = TelegramWebApp::instance().unwrap();
3455 let granted = Rc::new(Cell::new(true));
3456 let granted_clone = Rc::clone(&granted);
3457
3458 app.request_emoji_status_access(move |g| {
3459 granted_clone.set(g);
3460 })
3461 .unwrap();
3462
3463 assert!(!granted.get());
3464 }
3465
3466 #[wasm_bindgen_test]
3467 #[allow(dead_code, clippy::unused_unit)]
3468 fn set_emoji_status_invokes_callback() {
3469 let webapp = setup_webapp();
3470 let set_status = Function::new_with_args("status, cb", "this.st = status; cb(true);");
3471 let _ = Reflect::set(&webapp, &"setEmojiStatus".into(), &set_status);
3472
3473 let status = Object::new();
3474 let _ = Reflect::set(
3475 &status,
3476 &"custom_emoji_id".into(),
3477 &JsValue::from_str("321")
3478 );
3479
3480 let app = TelegramWebApp::instance().unwrap();
3481 let success = Rc::new(Cell::new(false));
3482 let success_clone = Rc::clone(&success);
3483
3484 app.set_emoji_status(&status.into(), move |s| {
3485 success_clone.set(s);
3486 })
3487 .unwrap();
3488
3489 assert!(success.get());
3490 let stored = Reflect::get(&webapp, &"st".into()).unwrap();
3491 let id = Reflect::get(&stored, &"custom_emoji_id".into())
3492 .unwrap()
3493 .as_string();
3494 assert_eq!(id.as_deref(), Some("321"));
3495 }
3496
3497 #[wasm_bindgen_test]
3498 #[allow(dead_code, clippy::unused_unit)]
3499 fn show_popup_invokes_callback() {
3500 let webapp = setup_webapp();
3501 let show_popup = Function::new_with_args("params, cb", "cb('ok');");
3502 let _ = Reflect::set(&webapp, &"showPopup".into(), &show_popup);
3503
3504 let app = TelegramWebApp::instance().unwrap();
3505 let button = Rc::new(RefCell::new(String::new()));
3506 let button_clone = Rc::clone(&button);
3507
3508 app.show_popup(&JsValue::NULL, move |id| {
3509 *button_clone.borrow_mut() = id;
3510 })
3511 .unwrap();
3512
3513 assert_eq!(button.borrow().as_str(), "ok");
3514 }
3515
3516 #[wasm_bindgen_test]
3517 #[allow(dead_code, clippy::unused_unit)]
3518 fn read_text_from_clipboard_invokes_callback() {
3519 let webapp = setup_webapp();
3520 let read_clip = Function::new_with_args("cb", "cb('clip');");
3521 let _ = Reflect::set(&webapp, &"readTextFromClipboard".into(), &read_clip);
3522
3523 let app = TelegramWebApp::instance().unwrap();
3524 let text = Rc::new(RefCell::new(String::new()));
3525 let text_clone = Rc::clone(&text);
3526
3527 app.read_text_from_clipboard(move |t| {
3528 *text_clone.borrow_mut() = t;
3529 })
3530 .unwrap();
3531
3532 assert_eq!(text.borrow().as_str(), "clip");
3533 }
3534
3535 #[wasm_bindgen_test]
3536 #[allow(dead_code, clippy::unused_unit)]
3537 fn scan_qr_popup_invokes_callback_and_close() {
3538 let webapp = setup_webapp();
3539 let show_scan = Function::new_with_args("text, cb", "cb('code');");
3540 let close_scan = Function::new_with_args("", "this.closed = true;");
3541 let _ = Reflect::set(&webapp, &"showScanQrPopup".into(), &show_scan);
3542 let _ = Reflect::set(&webapp, &"closeScanQrPopup".into(), &close_scan);
3543
3544 let app = TelegramWebApp::instance().unwrap();
3545 let text = Rc::new(RefCell::new(String::new()));
3546 let text_clone = Rc::clone(&text);
3547
3548 app.show_scan_qr_popup("scan", move |value| {
3549 *text_clone.borrow_mut() = value;
3550 })
3551 .unwrap();
3552 assert_eq!(text.borrow().as_str(), "code");
3553
3554 app.close_scan_qr_popup().unwrap();
3555 let closed = Reflect::get(&webapp, &"closed".into())
3556 .unwrap()
3557 .as_bool()
3558 .unwrap_or(false);
3559 assert!(closed);
3560 }
3561}