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