1use js_sys::{Function, Object, Reflect};
2use serde_wasm_bindgen::to_value;
3use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
4use web_sys::window;
5
6use crate::{
7 core::types::download_file_params::DownloadFileParams,
8 logger,
9 validate_init_data::{self, ValidationKey}
10};
11
12pub struct EventHandle<T: ?Sized> {
14 target: Object,
15 method: &'static str,
16 event: Option<String>,
17 callback: Closure<T>
18}
19
20impl<T: ?Sized> EventHandle<T> {
21 fn new(
22 target: Object,
23 method: &'static str,
24 event: Option<String>,
25 callback: Closure<T>
26 ) -> Self {
27 Self {
28 target,
29 method,
30 event,
31 callback
32 }
33 }
34
35 pub(crate) fn unregister(self) -> Result<(), JsValue> {
36 let f = Reflect::get(&self.target, &self.method.into())?;
37 let func = f
38 .dyn_ref::<Function>()
39 .ok_or_else(|| JsValue::from_str(&format!("{} is not a function", self.method)))?;
40 match self.event {
41 Some(event) => func.call2(
42 &self.target,
43 &event.into(),
44 self.callback.as_ref().unchecked_ref()
45 )?,
46 None => func.call1(&self.target, self.callback.as_ref().unchecked_ref())?
47 };
48 Ok(())
49 }
50}
51
52#[derive(Clone, Copy, Debug)]
54pub enum BottomButton {
55 Main,
57 Secondary
59}
60
61impl BottomButton {
62 const fn js_name(self) -> &'static str {
63 match self {
64 BottomButton::Main => "MainButton",
65 BottomButton::Secondary => "SecondaryButton"
66 }
67 }
68}
69
70#[derive(Clone, Copy, Debug)]
73pub enum BackgroundEvent {
74 MainButtonClicked,
76 BackButtonClicked,
78 SettingsButtonClicked,
80 WriteAccessRequested,
82 ContactRequested,
84 PhoneRequested,
86 InvoiceClosed,
88 PopupClosed,
90 QrTextReceived,
92 ClipboardTextReceived
94}
95
96impl BackgroundEvent {
97 const fn as_str(self) -> &'static str {
98 match self {
99 BackgroundEvent::MainButtonClicked => "mainButtonClicked",
100 BackgroundEvent::BackButtonClicked => "backButtonClicked",
101 BackgroundEvent::SettingsButtonClicked => "settingsButtonClicked",
102 BackgroundEvent::WriteAccessRequested => "writeAccessRequested",
103 BackgroundEvent::ContactRequested => "contactRequested",
104 BackgroundEvent::PhoneRequested => "phoneRequested",
105 BackgroundEvent::InvoiceClosed => "invoiceClosed",
106 BackgroundEvent::PopupClosed => "popupClosed",
107 BackgroundEvent::QrTextReceived => "qrTextReceived",
108 BackgroundEvent::ClipboardTextReceived => "clipboardTextReceived"
109 }
110 }
111}
112
113#[derive(Clone)]
115pub struct TelegramWebApp {
116 inner: Object
117}
118
119impl TelegramWebApp {
120 pub fn instance() -> Option<Self> {
122 let win = window()?;
123 let tg = Reflect::get(&win, &"Telegram".into()).ok()?;
124 let webapp = Reflect::get(&tg, &"WebApp".into()).ok()?;
125 webapp.dyn_into::<Object>().ok().map(|inner| Self {
126 inner
127 })
128 }
129
130 pub fn try_instance() -> Result<Self, JsValue> {
136 let win = window().ok_or_else(|| JsValue::from_str("window not available"))?;
137 let tg = Reflect::get(&win, &"Telegram".into())?;
138 let webapp = Reflect::get(&tg, &"WebApp".into())?;
139 let inner = webapp.dyn_into::<Object>()?;
140 Ok(Self {
141 inner
142 })
143 }
144
145 pub fn validate_init_data(
162 init_data: &str,
163 key: ValidationKey
164 ) -> Result<(), validate_init_data::ValidationError> {
165 match key {
166 ValidationKey::BotToken(token) => {
167 validate_init_data::verify_hmac_sha256(init_data, token)
168 }
169 ValidationKey::Ed25519PublicKey(pk) => {
170 validate_init_data::verify_ed25519(init_data, pk)
171 }
172 }
173 }
174
175 pub fn send_data(&self, data: &str) -> Result<(), JsValue> {
180 self.call1("sendData", &data.into())
181 }
182
183 pub fn expand(&self) -> Result<(), JsValue> {
188 self.call0("expand")
189 }
190
191 pub fn close(&self) -> Result<(), JsValue> {
196 self.call0("close")
197 }
198
199 pub fn enable_closing_confirmation(&self) -> Result<(), JsValue> {
211 self.call0("enableClosingConfirmation")
212 }
213
214 pub fn disable_closing_confirmation(&self) -> Result<(), JsValue> {
226 self.call0("disableClosingConfirmation")
227 }
228
229 pub fn is_closing_confirmation_enabled(&self) -> bool {
238 Reflect::get(&self.inner, &"isClosingConfirmationEnabled".into())
239 .ok()
240 .and_then(|v| v.as_bool())
241 .unwrap_or(false)
242 }
243
244 pub fn request_fullscreen(&self) -> Result<(), JsValue> {
256 self.call0("requestFullscreen")
257 }
258
259 pub fn exit_fullscreen(&self) -> Result<(), JsValue> {
271 self.call0("exitFullscreen")
272 }
273
274 pub fn lock_orientation(&self, orientation: &str) -> Result<(), JsValue> {
286 self.call1("lockOrientation", &orientation.into())
287 }
288
289 pub fn unlock_orientation(&self) -> Result<(), JsValue> {
301 self.call0("unlockOrientation")
302 }
303
304 pub fn enable_vertical_swipes(&self) -> Result<(), JsValue> {
316 self.call0("enableVerticalSwipes")
317 }
318
319 pub fn disable_vertical_swipes(&self) -> Result<(), JsValue> {
331 self.call0("disableVerticalSwipes")
332 }
333
334 pub fn show_alert(&self, msg: &str) -> Result<(), JsValue> {
339 self.call1("showAlert", &msg.into())
340 }
341
342 pub fn show_confirm<F>(&self, msg: &str, on_confirm: F) -> Result<(), JsValue>
347 where
348 F: 'static + Fn(bool)
349 {
350 let cb = Closure::<dyn FnMut(bool)>::new(on_confirm);
351 let f = Reflect::get(&self.inner, &"showConfirm".into())?;
352 let func = f
353 .dyn_ref::<Function>()
354 .ok_or_else(|| JsValue::from_str("showConfirm is not a function"))?;
355 func.call2(&self.inner, &msg.into(), cb.as_ref().unchecked_ref())?;
356 cb.forget(); Ok(())
358 }
359
360 pub fn open_link(&self, url: &str) -> Result<(), JsValue> {
369 Reflect::get(&self.inner, &"openLink".into())?
370 .dyn_into::<Function>()?
371 .call1(&self.inner, &url.into())?;
372 Ok(())
373 }
374
375 pub fn open_telegram_link(&self, url: &str) -> Result<(), JsValue> {
384 Reflect::get(&self.inner, &"openTelegramLink".into())?
385 .dyn_into::<Function>()?
386 .call1(&self.inner, &url.into())?;
387 Ok(())
388 }
389
390 pub fn open_invoice<F>(&self, url: &str, callback: F) -> Result<(), JsValue>
402 where
403 F: 'static + Fn(String)
404 {
405 let cb = Closure::<dyn FnMut(JsValue)>::new(move |status: JsValue| {
406 callback(status.as_string().unwrap_or_default());
407 });
408 Reflect::get(&self.inner, &"openInvoice".into())?
409 .dyn_into::<Function>()?
410 .call2(&self.inner, &url.into(), cb.as_ref().unchecked_ref())?;
411 cb.forget();
412 Ok(())
413 }
414
415 pub fn switch_inline_query(
427 &self,
428 query: &str,
429 choose_chat_types: Option<&JsValue>
430 ) -> Result<(), JsValue> {
431 let f = Reflect::get(&self.inner, &"switchInlineQuery".into())?;
432 let func = f
433 .dyn_ref::<Function>()
434 .ok_or_else(|| JsValue::from_str("switchInlineQuery is not a function"))?;
435 match choose_chat_types {
436 Some(types) => func.call2(&self.inner, &query.into(), types)?,
437 None => func.call1(&self.inner, &query.into())?
438 };
439 Ok(())
440 }
441
442 pub fn share_message<F>(&self, msg_id: &str, callback: F) -> Result<(), JsValue>
457 where
458 F: 'static + Fn(bool)
459 {
460 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
461 callback(v.as_bool().unwrap_or(false));
462 });
463 let f = Reflect::get(&self.inner, &"shareMessage".into())?;
464 let func = f
465 .dyn_ref::<Function>()
466 .ok_or_else(|| JsValue::from_str("shareMessage is not a function"))?;
467 func.call2(&self.inner, &msg_id.into(), cb.as_ref().unchecked_ref())?;
468 cb.forget();
469 Ok(())
470 }
471
472 pub fn share_to_story(
487 &self,
488 media_url: &str,
489 params: Option<&JsValue>
490 ) -> Result<(), JsValue> {
491 let f = Reflect::get(&self.inner, &"shareToStory".into())?;
492 let func = f
493 .dyn_ref::<Function>()
494 .ok_or_else(|| JsValue::from_str("shareToStory is not a function"))?;
495 match params {
496 Some(p) => func.call2(&self.inner, &media_url.into(), p)?,
497 None => func.call1(&self.inner, &media_url.into())?
498 };
499 Ok(())
500 }
501
502 pub fn share_url(&self, url: &str, text: Option<&str>) -> Result<(), JsValue> {
515 let f = Reflect::get(&self.inner, &"shareURL".into())?;
516 let func = f
517 .dyn_ref::<Function>()
518 .ok_or_else(|| JsValue::from_str("shareURL is not a function"))?;
519 match text {
520 Some(t) => func.call2(&self.inner, &url.into(), &t.into())?,
521 None => func.call1(&self.inner, &url.into())?
522 };
523 Ok(())
524 }
525
526 pub fn join_voice_chat(
538 &self,
539 chat_id: &str,
540 invite_hash: Option<&str>
541 ) -> Result<(), JsValue> {
542 let f = Reflect::get(&self.inner, &"joinVoiceChat".into())?;
543 let func = f
544 .dyn_ref::<Function>()
545 .ok_or_else(|| JsValue::from_str("joinVoiceChat is not a function"))?;
546 match invite_hash {
547 Some(hash) => func.call2(&self.inner, &chat_id.into(), &hash.into())?,
548 None => func.call1(&self.inner, &chat_id.into())?
549 };
550 Ok(())
551 }
552
553 pub fn add_to_home_screen(&self) -> Result<bool, JsValue> {
562 let f = Reflect::get(&self.inner, &"addToHomeScreen".into())?;
563 let func = f
564 .dyn_ref::<Function>()
565 .ok_or_else(|| JsValue::from_str("addToHomeScreen is not a function"))?;
566 let result = func.call0(&self.inner)?;
567 Ok(result.as_bool().unwrap_or(false))
568 }
569
570 pub fn check_home_screen_status<F>(&self, callback: F) -> Result<(), JsValue>
582 where
583 F: 'static + Fn(String)
584 {
585 let cb = Closure::<dyn FnMut(JsValue)>::new(move |status: JsValue| {
586 callback(status.as_string().unwrap_or_default());
587 });
588 let f = Reflect::get(&self.inner, &"checkHomeScreenStatus".into())?;
589 let func = f
590 .dyn_ref::<Function>()
591 .ok_or_else(|| JsValue::from_str("checkHomeScreenStatus is not a function"))?;
592 func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
593 cb.forget();
594 Ok(())
595 }
596
597 pub fn request_write_access<F>(&self, callback: F) -> Result<(), JsValue>
612 where
613 F: 'static + Fn(bool)
614 {
615 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
616 callback(v.as_bool().unwrap_or(false));
617 });
618 self.call1("requestWriteAccess", cb.as_ref().unchecked_ref())?;
619 cb.forget();
620 Ok(())
621 }
622
623 pub fn download_file<F>(
645 &self,
646 params: DownloadFileParams<'_>,
647 callback: F
648 ) -> Result<(), JsValue>
649 where
650 F: 'static + Fn(String)
651 {
652 let js_params =
653 to_value(¶ms).map_err(|e| JsValue::from_str(&format!("serialize params: {e}")))?;
654 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
655 callback(v.as_string().unwrap_or_default());
656 });
657 Reflect::get(&self.inner, &"downloadFile".into())?
658 .dyn_into::<Function>()?
659 .call2(&self.inner, &js_params, cb.as_ref().unchecked_ref())?;
660 cb.forget();
661 Ok(())
662 }
663
664 pub fn request_emoji_status_access<F>(&self, callback: F) -> Result<(), JsValue>
679 where
680 F: 'static + Fn(bool)
681 {
682 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
683 callback(v.as_bool().unwrap_or(false));
684 });
685 let f = Reflect::get(&self.inner, &"requestEmojiStatusAccess".into())?;
686 let func = f
687 .dyn_ref::<Function>()
688 .ok_or_else(|| JsValue::from_str("requestEmojiStatusAccess is not a function"))?;
689 func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
690 cb.forget();
691 Ok(())
692 }
693
694 pub fn set_emoji_status<F>(&self, status: &JsValue, callback: F) -> Result<(), JsValue>
713 where
714 F: 'static + Fn(bool)
715 {
716 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
717 callback(v.as_bool().unwrap_or(false));
718 });
719 let f = Reflect::get(&self.inner, &"setEmojiStatus".into())?;
720 let func = f
721 .dyn_ref::<Function>()
722 .ok_or_else(|| JsValue::from_str("setEmojiStatus is not a function"))?;
723 func.call2(&self.inner, status, cb.as_ref().unchecked_ref())?;
724 cb.forget();
725 Ok(())
726 }
727
728 pub fn show_popup<F>(&self, params: &JsValue, callback: F) -> Result<(), JsValue>
742 where
743 F: 'static + Fn(String)
744 {
745 let cb = Closure::<dyn FnMut(JsValue)>::new(move |id: JsValue| {
746 callback(id.as_string().unwrap_or_default());
747 });
748 Reflect::get(&self.inner, &"showPopup".into())?
749 .dyn_into::<Function>()?
750 .call2(&self.inner, params, cb.as_ref().unchecked_ref())?;
751 cb.forget();
752 Ok(())
753 }
754
755 pub fn show_scan_qr_popup<F>(&self, text: &str, callback: F) -> Result<(), JsValue>
767 where
768 F: 'static + Fn(String)
769 {
770 let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
771 callback(value.as_string().unwrap_or_default());
772 });
773 Reflect::get(&self.inner, &"showScanQrPopup".into())?
774 .dyn_into::<Function>()?
775 .call2(&self.inner, &text.into(), cb.as_ref().unchecked_ref())?;
776 cb.forget();
777 Ok(())
778 }
779
780 pub fn close_scan_qr_popup(&self) -> Result<(), JsValue> {
789 Reflect::get(&self.inner, &"closeScanQrPopup".into())?
790 .dyn_into::<Function>()?
791 .call0(&self.inner)?;
792 Ok(())
793 }
794
795 fn bottom_button_object(&self, button: BottomButton) -> Result<Object, JsValue> {
797 let name = button.js_name();
798 Reflect::get(&self.inner, &name.into())
799 .inspect_err(|_| logger::error(&format!("{name} not available")))?
800 .dyn_into::<Object>()
801 .inspect_err(|_| logger::error(&format!("{name} is not an object")))
802 }
803
804 fn bottom_button_method(
805 &self,
806 button: BottomButton,
807 method: &str,
808 arg: Option<&JsValue>
809 ) -> Result<(), JsValue> {
810 let name = button.js_name();
811 let btn = self.bottom_button_object(button)?;
812 let f = Reflect::get(&btn, &method.into())
813 .inspect_err(|_| logger::error(&format!("{name}.{method} not available")))?;
814 let func = f.dyn_ref::<Function>().ok_or_else(|| {
815 logger::error(&format!("{name}.{method} is not a function"));
816 JsValue::from_str("not a function")
817 })?;
818 let result = match arg {
819 Some(v) => func.call1(&btn, v),
820 None => func.call0(&btn)
821 };
822 result.inspect_err(|_| logger::error(&format!("{name}.{method} call failed")))?;
823 Ok(())
824 }
825
826 pub fn hide_keyboard(&self) -> Result<(), JsValue> {
839 self.call0("hideKeyboard")
840 }
841
842 pub fn read_text_from_clipboard<F>(&self, callback: F) -> Result<(), JsValue>
857 where
858 F: 'static + Fn(String)
859 {
860 let cb = Closure::<dyn FnMut(JsValue)>::new(move |text: JsValue| {
861 callback(text.as_string().unwrap_or_default());
862 });
863 let f = Reflect::get(&self.inner, &"readTextFromClipboard".into())?;
864 let func = f
865 .dyn_ref::<Function>()
866 .ok_or_else(|| JsValue::from_str("readTextFromClipboard is not a function"))?;
867 func.call1(&self.inner, cb.as_ref().unchecked_ref())?;
868 cb.forget();
869 Ok(())
870 }
871
872 pub fn show_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
877 self.bottom_button_method(button, "show", None)
878 }
879
880 pub fn hide_bottom_button(&self, button: BottomButton) -> Result<(), JsValue> {
885 self.bottom_button_method(button, "hide", None)
886 }
887
888 pub fn ready(&self) -> Result<(), JsValue> {
893 self.call0("ready")
894 }
895
896 pub fn show_back_button(&self) -> Result<(), JsValue> {
901 self.call_nested0("BackButton", "show")
902 }
903
904 pub fn hide_back_button(&self) -> Result<(), JsValue> {
909 self.call_nested0("BackButton", "hide")
910 }
911
912 pub fn set_header_color(&self, color: &str) -> Result<(), JsValue> {
924 self.call1("setHeaderColor", &color.into())
925 }
926
927 pub fn set_background_color(&self, color: &str) -> Result<(), JsValue> {
939 self.call1("setBackgroundColor", &color.into())
940 }
941
942 pub fn set_bottom_bar_color(&self, color: &str) -> Result<(), JsValue> {
954 self.call1("setBottomBarColor", &color.into())
955 }
956
957 pub fn set_bottom_button_text(&self, button: BottomButton, text: &str) -> Result<(), JsValue> {
962 self.bottom_button_method(button, "setText", Some(&text.into()))
963 }
964
965 pub fn set_bottom_button_color(
977 &self,
978 button: BottomButton,
979 color: &str
980 ) -> Result<(), JsValue> {
981 self.bottom_button_method(button, "setColor", Some(&color.into()))
982 }
983
984 pub fn set_bottom_button_text_color(
996 &self,
997 button: BottomButton,
998 color: &str
999 ) -> Result<(), JsValue> {
1000 self.bottom_button_method(button, "setTextColor", Some(&color.into()))
1001 }
1002
1003 pub fn set_bottom_button_callback<F>(
1010 &self,
1011 button: BottomButton,
1012 callback: F
1013 ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1014 where
1015 F: 'static + Fn()
1016 {
1017 let btn_val = Reflect::get(&self.inner, &button.js_name().into())?;
1018 let btn = btn_val.dyn_into::<Object>()?;
1019 let cb = Closure::<dyn FnMut()>::new(callback);
1020 let f = Reflect::get(&btn, &"onClick".into())?;
1021 let func = f
1022 .dyn_ref::<Function>()
1023 .ok_or_else(|| JsValue::from_str("onClick is not a function"))?;
1024 func.call1(&btn, cb.as_ref().unchecked_ref())?;
1025 Ok(EventHandle::new(btn, "offClick", None, cb))
1026 }
1027
1028 pub fn remove_bottom_button_callback(
1033 &self,
1034 handle: EventHandle<dyn FnMut()>
1035 ) -> Result<(), JsValue> {
1036 handle.unregister()
1037 }
1038
1039 pub fn show_main_button(&self) -> Result<(), JsValue> {
1042 self.show_bottom_button(BottomButton::Main)
1043 }
1044
1045 pub fn show_secondary_button(&self) -> Result<(), JsValue> {
1047 self.show_bottom_button(BottomButton::Secondary)
1048 }
1049
1050 pub fn hide_main_button(&self) -> Result<(), JsValue> {
1053 self.hide_bottom_button(BottomButton::Main)
1054 }
1055
1056 pub fn hide_secondary_button(&self) -> Result<(), JsValue> {
1058 self.hide_bottom_button(BottomButton::Secondary)
1059 }
1060
1061 pub fn set_main_button_text(&self, text: &str) -> Result<(), JsValue> {
1064 self.set_bottom_button_text(BottomButton::Main, text)
1065 }
1066
1067 pub fn set_secondary_button_text(&self, text: &str) -> Result<(), JsValue> {
1069 self.set_bottom_button_text(BottomButton::Secondary, text)
1070 }
1071
1072 pub fn set_main_button_color(&self, color: &str) -> Result<(), JsValue> {
1075 self.set_bottom_button_color(BottomButton::Main, color)
1076 }
1077
1078 pub fn set_secondary_button_color(&self, color: &str) -> Result<(), JsValue> {
1080 self.set_bottom_button_color(BottomButton::Secondary, color)
1081 }
1082
1083 pub fn set_main_button_text_color(&self, color: &str) -> Result<(), JsValue> {
1086 self.set_bottom_button_text_color(BottomButton::Main, color)
1087 }
1088
1089 pub fn set_secondary_button_text_color(&self, color: &str) -> Result<(), JsValue> {
1091 self.set_bottom_button_text_color(BottomButton::Secondary, color)
1092 }
1093
1094 pub fn set_main_button_callback<F>(
1097 &self,
1098 callback: F
1099 ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1100 where
1101 F: 'static + Fn()
1102 {
1103 self.set_bottom_button_callback(BottomButton::Main, callback)
1104 }
1105
1106 pub fn set_secondary_button_callback<F>(
1108 &self,
1109 callback: F
1110 ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1111 where
1112 F: 'static + Fn()
1113 {
1114 self.set_bottom_button_callback(BottomButton::Secondary, callback)
1115 }
1116
1117 pub fn remove_main_button_callback(
1119 &self,
1120 handle: EventHandle<dyn FnMut()>
1121 ) -> Result<(), JsValue> {
1122 self.remove_bottom_button_callback(handle)
1123 }
1124
1125 pub fn remove_secondary_button_callback(
1127 &self,
1128 handle: EventHandle<dyn FnMut()>
1129 ) -> Result<(), JsValue> {
1130 self.remove_bottom_button_callback(handle)
1131 }
1132
1133 pub fn on_event<F>(
1141 &self,
1142 event: &str,
1143 callback: F
1144 ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
1145 where
1146 F: 'static + Fn(JsValue)
1147 {
1148 let cb = Closure::<dyn FnMut(JsValue)>::new(callback);
1149 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1150 let func = f
1151 .dyn_ref::<Function>()
1152 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1153 func.call2(&self.inner, &event.into(), cb.as_ref().unchecked_ref())?;
1154 Ok(EventHandle::new(
1155 self.inner.clone(),
1156 "offEvent",
1157 Some(event.to_owned()),
1158 cb
1159 ))
1160 }
1161
1162 pub fn on_background_event<F>(
1170 &self,
1171 event: BackgroundEvent,
1172 callback: F
1173 ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
1174 where
1175 F: 'static + Fn(JsValue)
1176 {
1177 let cb = Closure::<dyn FnMut(JsValue)>::new(callback);
1178 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1179 let func = f
1180 .dyn_ref::<Function>()
1181 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1182 func.call2(
1183 &self.inner,
1184 &event.as_str().into(),
1185 cb.as_ref().unchecked_ref()
1186 )?;
1187 Ok(EventHandle::new(
1188 self.inner.clone(),
1189 "offEvent",
1190 Some(event.as_str().to_string()),
1191 cb
1192 ))
1193 }
1194
1195 pub fn off_event<T: ?Sized>(&self, handle: EventHandle<T>) -> Result<(), JsValue> {
1200 handle.unregister()
1201 }
1202
1203 fn call_nested0(&self, field: &str, method: &str) -> Result<(), JsValue> {
1205 let obj = Reflect::get(&self.inner, &field.into())?;
1206 let f = Reflect::get(&obj, &method.into())?;
1207 let func = f
1208 .dyn_ref::<Function>()
1209 .ok_or_else(|| JsValue::from_str("not a function"))?;
1210 func.call0(&obj)?;
1211 Ok(())
1212 }
1213
1214 fn call0(&self, method: &str) -> Result<(), JsValue> {
1217 let f = Reflect::get(&self.inner, &method.into())?;
1218 let func = f
1219 .dyn_ref::<Function>()
1220 .ok_or_else(|| JsValue::from_str("not a function"))?;
1221 func.call0(&self.inner)?;
1222 Ok(())
1223 }
1224
1225 fn call1(&self, method: &str, arg: &JsValue) -> Result<(), JsValue> {
1226 let f = Reflect::get(&self.inner, &method.into())?;
1227 let func = f
1228 .dyn_ref::<Function>()
1229 .ok_or_else(|| JsValue::from_str("not a function"))?;
1230 func.call1(&self.inner, arg)?;
1231 Ok(())
1232 }
1233
1234 pub fn viewport_height(&self) -> Option<f64> {
1243 Reflect::get(&self.inner, &"viewportHeight".into())
1244 .ok()?
1245 .as_f64()
1246 }
1247
1248 pub fn viewport_width(&self) -> Option<f64> {
1257 Reflect::get(&self.inner, &"viewportWidth".into())
1258 .ok()?
1259 .as_f64()
1260 }
1261
1262 pub fn viewport_stable_height(&self) -> Option<f64> {
1271 Reflect::get(&self.inner, &"viewportStableHeight".into())
1272 .ok()?
1273 .as_f64()
1274 }
1275
1276 pub fn is_expanded(&self) -> bool {
1277 Reflect::get(&self.inner, &"isExpanded".into())
1278 .ok()
1279 .and_then(|v| v.as_bool())
1280 .unwrap_or(false)
1281 }
1282
1283 pub fn expand_viewport(&self) -> Result<(), JsValue> {
1288 self.call0("expand")
1289 }
1290
1291 pub fn on_theme_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
1299 where
1300 F: 'static + Fn()
1301 {
1302 let cb = Closure::<dyn FnMut()>::new(callback);
1303 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1304 let func = f
1305 .dyn_ref::<Function>()
1306 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1307 func.call2(
1308 &self.inner,
1309 &"themeChanged".into(),
1310 cb.as_ref().unchecked_ref()
1311 )?;
1312 Ok(EventHandle::new(
1313 self.inner.clone(),
1314 "offEvent",
1315 Some("themeChanged".to_string()),
1316 cb
1317 ))
1318 }
1319
1320 pub fn on_safe_area_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
1328 where
1329 F: 'static + Fn()
1330 {
1331 let cb = Closure::<dyn FnMut()>::new(callback);
1332 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1333 let func = f
1334 .dyn_ref::<Function>()
1335 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1336 func.call2(
1337 &self.inner,
1338 &"safeAreaChanged".into(),
1339 cb.as_ref().unchecked_ref()
1340 )?;
1341 Ok(EventHandle::new(
1342 self.inner.clone(),
1343 "offEvent",
1344 Some("safeAreaChanged".to_string()),
1345 cb
1346 ))
1347 }
1348
1349 pub fn on_content_safe_area_changed<F>(
1357 &self,
1358 callback: F
1359 ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1360 where
1361 F: 'static + Fn()
1362 {
1363 let cb = Closure::<dyn FnMut()>::new(callback);
1364 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1365 let func = f
1366 .dyn_ref::<Function>()
1367 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1368 func.call2(
1369 &self.inner,
1370 &"contentSafeAreaChanged".into(),
1371 cb.as_ref().unchecked_ref()
1372 )?;
1373 Ok(EventHandle::new(
1374 self.inner.clone(),
1375 "offEvent",
1376 Some("contentSafeAreaChanged".to_string()),
1377 cb
1378 ))
1379 }
1380
1381 pub fn on_viewport_changed<F>(&self, callback: F) -> Result<EventHandle<dyn FnMut()>, JsValue>
1389 where
1390 F: 'static + Fn()
1391 {
1392 let cb = Closure::<dyn FnMut()>::new(callback);
1393 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1394 let func = f
1395 .dyn_ref::<Function>()
1396 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1397 func.call2(
1398 &self.inner,
1399 &"viewportChanged".into(),
1400 cb.as_ref().unchecked_ref()
1401 )?;
1402 Ok(EventHandle::new(
1403 self.inner.clone(),
1404 "offEvent",
1405 Some("viewportChanged".to_string()),
1406 cb
1407 ))
1408 }
1409
1410 pub fn on_clipboard_text_received<F>(
1418 &self,
1419 callback: F
1420 ) -> Result<EventHandle<dyn FnMut(JsValue)>, JsValue>
1421 where
1422 F: 'static + Fn(String)
1423 {
1424 let cb = Closure::<dyn FnMut(JsValue)>::new(move |text: JsValue| {
1425 callback(text.as_string().unwrap_or_default());
1426 });
1427 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1428 let func = f
1429 .dyn_ref::<Function>()
1430 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1431 func.call2(
1432 &self.inner,
1433 &"clipboardTextReceived".into(),
1434 cb.as_ref().unchecked_ref()
1435 )?;
1436 Ok(EventHandle::new(
1437 self.inner.clone(),
1438 "offEvent",
1439 Some("clipboardTextReceived".to_string()),
1440 cb
1441 ))
1442 }
1443
1444 pub fn on_invoice_closed<F>(
1464 &self,
1465 callback: F
1466 ) -> Result<EventHandle<dyn FnMut(String)>, JsValue>
1467 where
1468 F: 'static + Fn(String)
1469 {
1470 let cb = Closure::<dyn FnMut(String)>::new(callback);
1471 let f = Reflect::get(&self.inner, &"onEvent".into())?;
1472 let func = f
1473 .dyn_ref::<Function>()
1474 .ok_or_else(|| JsValue::from_str("onEvent is not a function"))?;
1475 func.call2(
1476 &self.inner,
1477 &"invoiceClosed".into(),
1478 cb.as_ref().unchecked_ref()
1479 )?;
1480 Ok(EventHandle::new(
1481 self.inner.clone(),
1482 "offEvent",
1483 Some("invoiceClosed".to_string()),
1484 cb
1485 ))
1486 }
1487
1488 pub fn set_back_button_callback<F>(
1504 &self,
1505 callback: F
1506 ) -> Result<EventHandle<dyn FnMut()>, JsValue>
1507 where
1508 F: 'static + Fn()
1509 {
1510 let back_button_val = Reflect::get(&self.inner, &"BackButton".into())?;
1511 let back_button = back_button_val.dyn_into::<Object>()?;
1512 let cb = Closure::<dyn FnMut()>::new(callback);
1513 let f = Reflect::get(&back_button, &"onClick".into())?;
1514 let func = f
1515 .dyn_ref::<Function>()
1516 .ok_or_else(|| JsValue::from_str("onClick is not a function"))?;
1517 func.call1(&back_button, cb.as_ref().unchecked_ref())?;
1518 Ok(EventHandle::new(back_button, "offClick", None, cb))
1519 }
1520
1521 pub fn remove_back_button_callback(
1526 &self,
1527 handle: EventHandle<dyn FnMut()>
1528 ) -> Result<(), JsValue> {
1529 handle.unregister()
1530 }
1531 pub fn is_back_button_visible(&self) -> bool {
1540 Reflect::get(&self.inner, &"BackButton".into())
1541 .ok()
1542 .and_then(|bb| Reflect::get(&bb, &"isVisible".into()).ok())
1543 .and_then(|v| v.as_bool())
1544 .unwrap_or(false)
1545 }
1546}
1547
1548#[cfg(test)]
1549mod tests {
1550 use std::{
1551 cell::{Cell, RefCell},
1552 rc::Rc
1553 };
1554
1555 use js_sys::{Function, Object, Reflect};
1556 use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
1557 use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
1558 use web_sys::window;
1559
1560 use super::*;
1561
1562 wasm_bindgen_test_configure!(run_in_browser);
1563
1564 #[allow(dead_code)]
1565 fn setup_webapp() -> Object {
1566 let win = window().unwrap();
1567 let telegram = Object::new();
1568 let webapp = Object::new();
1569 let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
1570 let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
1571 webapp
1572 }
1573
1574 #[wasm_bindgen_test]
1575 #[allow(dead_code, clippy::unused_unit)]
1576 fn hide_keyboard_calls_js() {
1577 let webapp = setup_webapp();
1578 let called = Rc::new(Cell::new(false));
1579 let called_clone = Rc::clone(&called);
1580
1581 let hide_cb = Closure::<dyn FnMut()>::new(move || {
1582 called_clone.set(true);
1583 });
1584 let _ = Reflect::set(
1585 &webapp,
1586 &"hideKeyboard".into(),
1587 hide_cb.as_ref().unchecked_ref()
1588 );
1589 hide_cb.forget();
1590
1591 let app = TelegramWebApp::instance().unwrap();
1592 app.hide_keyboard().unwrap();
1593 assert!(called.get());
1594 }
1595
1596 #[wasm_bindgen_test]
1597 #[allow(dead_code, clippy::unused_unit)]
1598 fn hide_main_button_calls_js() {
1599 let webapp = setup_webapp();
1600 let main_button = Object::new();
1601 let called = Rc::new(Cell::new(false));
1602 let called_clone = Rc::clone(&called);
1603
1604 let hide_cb = Closure::<dyn FnMut()>::new(move || {
1605 called_clone.set(true);
1606 });
1607 let _ = Reflect::set(
1608 &main_button,
1609 &"hide".into(),
1610 hide_cb.as_ref().unchecked_ref()
1611 );
1612 hide_cb.forget();
1613
1614 let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
1615
1616 let app = TelegramWebApp::instance().unwrap();
1617 app.hide_bottom_button(BottomButton::Main).unwrap();
1618 assert!(called.get());
1619 }
1620
1621 #[wasm_bindgen_test]
1622 #[allow(dead_code, clippy::unused_unit)]
1623 fn hide_secondary_button_calls_js() {
1624 let webapp = setup_webapp();
1625 let secondary_button = Object::new();
1626 let called = Rc::new(Cell::new(false));
1627 let called_clone = Rc::clone(&called);
1628
1629 let hide_cb = Closure::<dyn FnMut()>::new(move || {
1630 called_clone.set(true);
1631 });
1632 let _ = Reflect::set(
1633 &secondary_button,
1634 &"hide".into(),
1635 hide_cb.as_ref().unchecked_ref()
1636 );
1637 hide_cb.forget();
1638
1639 let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
1640
1641 let app = TelegramWebApp::instance().unwrap();
1642 app.hide_bottom_button(BottomButton::Secondary).unwrap();
1643 assert!(called.get());
1644 }
1645
1646 #[wasm_bindgen_test]
1647 #[allow(dead_code, clippy::unused_unit)]
1648 fn set_bottom_button_color_calls_js() {
1649 let webapp = setup_webapp();
1650 let main_button = Object::new();
1651 let received = Rc::new(RefCell::new(None));
1652 let rc_clone = Rc::clone(&received);
1653
1654 let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1655 *rc_clone.borrow_mut() = v.as_string();
1656 });
1657 let _ = Reflect::set(
1658 &main_button,
1659 &"setColor".into(),
1660 set_color_cb.as_ref().unchecked_ref()
1661 );
1662 set_color_cb.forget();
1663
1664 let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
1665
1666 let app = TelegramWebApp::instance().unwrap();
1667 app.set_bottom_button_color(BottomButton::Main, "#00ff00")
1668 .unwrap();
1669 assert_eq!(received.borrow().as_deref(), Some("#00ff00"));
1670 }
1671
1672 #[wasm_bindgen_test]
1673 #[allow(dead_code, clippy::unused_unit)]
1674 fn set_secondary_button_color_calls_js() {
1675 let webapp = setup_webapp();
1676 let secondary_button = Object::new();
1677 let received = Rc::new(RefCell::new(None));
1678 let rc_clone = Rc::clone(&received);
1679
1680 let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1681 *rc_clone.borrow_mut() = v.as_string();
1682 });
1683 let _ = Reflect::set(
1684 &secondary_button,
1685 &"setColor".into(),
1686 set_color_cb.as_ref().unchecked_ref()
1687 );
1688 set_color_cb.forget();
1689
1690 let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
1691
1692 let app = TelegramWebApp::instance().unwrap();
1693 app.set_bottom_button_color(BottomButton::Secondary, "#00ff00")
1694 .unwrap();
1695 assert_eq!(received.borrow().as_deref(), Some("#00ff00"));
1696 }
1697
1698 #[wasm_bindgen_test]
1699 #[allow(dead_code, clippy::unused_unit)]
1700 fn set_bottom_button_text_color_calls_js() {
1701 let webapp = setup_webapp();
1702 let main_button = Object::new();
1703 let received = Rc::new(RefCell::new(None));
1704 let rc_clone = Rc::clone(&received);
1705
1706 let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1707 *rc_clone.borrow_mut() = v.as_string();
1708 });
1709 let _ = Reflect::set(
1710 &main_button,
1711 &"setTextColor".into(),
1712 set_color_cb.as_ref().unchecked_ref()
1713 );
1714 set_color_cb.forget();
1715
1716 let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
1717
1718 let app = TelegramWebApp::instance().unwrap();
1719 app.set_bottom_button_text_color(BottomButton::Main, "#112233")
1720 .unwrap();
1721 assert_eq!(received.borrow().as_deref(), Some("#112233"));
1722 }
1723
1724 #[wasm_bindgen_test]
1725 #[allow(dead_code, clippy::unused_unit)]
1726 fn set_secondary_button_text_color_calls_js() {
1727 let webapp = setup_webapp();
1728 let secondary_button = Object::new();
1729 let received = Rc::new(RefCell::new(None));
1730 let rc_clone = Rc::clone(&received);
1731
1732 let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1733 *rc_clone.borrow_mut() = v.as_string();
1734 });
1735 let _ = Reflect::set(
1736 &secondary_button,
1737 &"setTextColor".into(),
1738 set_color_cb.as_ref().unchecked_ref()
1739 );
1740 set_color_cb.forget();
1741
1742 let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
1743
1744 let app = TelegramWebApp::instance().unwrap();
1745 app.set_bottom_button_text_color(BottomButton::Secondary, "#112233")
1746 .unwrap();
1747 assert_eq!(received.borrow().as_deref(), Some("#112233"));
1748 }
1749
1750 #[wasm_bindgen_test]
1751 #[allow(dead_code, clippy::unused_unit)]
1752 fn set_header_color_calls_js() {
1753 let webapp = setup_webapp();
1754 let received = Rc::new(RefCell::new(None));
1755 let rc_clone = Rc::clone(&received);
1756
1757 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1758 *rc_clone.borrow_mut() = v.as_string();
1759 });
1760 let _ = Reflect::set(
1761 &webapp,
1762 &"setHeaderColor".into(),
1763 cb.as_ref().unchecked_ref()
1764 );
1765 cb.forget();
1766
1767 let app = TelegramWebApp::instance().unwrap();
1768 app.set_header_color("#abcdef").unwrap();
1769 assert_eq!(received.borrow().as_deref(), Some("#abcdef"));
1770 }
1771
1772 #[wasm_bindgen_test]
1773 #[allow(dead_code, clippy::unused_unit)]
1774 fn set_background_color_calls_js() {
1775 let webapp = setup_webapp();
1776 let received = Rc::new(RefCell::new(None));
1777 let rc_clone = Rc::clone(&received);
1778
1779 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1780 *rc_clone.borrow_mut() = v.as_string();
1781 });
1782 let _ = Reflect::set(
1783 &webapp,
1784 &"setBackgroundColor".into(),
1785 cb.as_ref().unchecked_ref()
1786 );
1787 cb.forget();
1788
1789 let app = TelegramWebApp::instance().unwrap();
1790 app.set_background_color("#123456").unwrap();
1791 assert_eq!(received.borrow().as_deref(), Some("#123456"));
1792 }
1793
1794 #[wasm_bindgen_test]
1795 #[allow(dead_code, clippy::unused_unit)]
1796 fn set_bottom_bar_color_calls_js() {
1797 let webapp = setup_webapp();
1798 let received = Rc::new(RefCell::new(None));
1799 let rc_clone = Rc::clone(&received);
1800
1801 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1802 *rc_clone.borrow_mut() = v.as_string();
1803 });
1804 let _ = Reflect::set(
1805 &webapp,
1806 &"setBottomBarColor".into(),
1807 cb.as_ref().unchecked_ref()
1808 );
1809 cb.forget();
1810
1811 let app = TelegramWebApp::instance().unwrap();
1812 app.set_bottom_bar_color("#654321").unwrap();
1813 assert_eq!(received.borrow().as_deref(), Some("#654321"));
1814 }
1815
1816 #[wasm_bindgen_test]
1817 #[allow(dead_code, clippy::unused_unit)]
1818 fn viewport_dimensions() {
1819 let webapp = setup_webapp();
1820 let _ = Reflect::set(&webapp, &"viewportWidth".into(), &JsValue::from_f64(320.0));
1821 let _ = Reflect::set(
1822 &webapp,
1823 &"viewportStableHeight".into(),
1824 &JsValue::from_f64(480.0)
1825 );
1826 let app = TelegramWebApp::instance().unwrap();
1827 assert_eq!(app.viewport_width(), Some(320.0));
1828 assert_eq!(app.viewport_stable_height(), Some(480.0));
1829 }
1830
1831 #[wasm_bindgen_test]
1832 #[allow(dead_code, clippy::unused_unit)]
1833 fn back_button_visibility_and_callback() {
1834 let webapp = setup_webapp();
1835 let back_button = Object::new();
1836 let _ = Reflect::set(&webapp, &"BackButton".into(), &back_button);
1837 let _ = Reflect::set(&back_button, &"isVisible".into(), &JsValue::TRUE);
1838
1839 let on_click = Function::new_with_args("cb", "this.cb = cb;");
1840 let off_click = Function::new_with_args("", "delete this.cb;");
1841 let _ = Reflect::set(&back_button, &"onClick".into(), &on_click);
1842 let _ = Reflect::set(&back_button, &"offClick".into(), &off_click);
1843
1844 let called = Rc::new(Cell::new(false));
1845 let called_clone = Rc::clone(&called);
1846
1847 let app = TelegramWebApp::instance().unwrap();
1848 assert!(app.is_back_button_visible());
1849 let handle = app
1850 .set_back_button_callback(move || {
1851 called_clone.set(true);
1852 })
1853 .unwrap();
1854
1855 let stored = Reflect::has(&back_button, &"cb".into()).unwrap();
1856 assert!(stored);
1857
1858 let cb_fn = Reflect::get(&back_button, &"cb".into())
1859 .unwrap()
1860 .dyn_into::<Function>()
1861 .unwrap();
1862 let _ = cb_fn.call0(&JsValue::NULL);
1863 assert!(called.get());
1864
1865 app.remove_back_button_callback(handle).unwrap();
1866 let stored_after = Reflect::has(&back_button, &"cb".into()).unwrap();
1867 assert!(!stored_after);
1868 }
1869
1870 #[wasm_bindgen_test]
1871 #[allow(dead_code, clippy::unused_unit)]
1872 fn bottom_button_callback_register_and_remove() {
1873 let webapp = setup_webapp();
1874 let main_button = Object::new();
1875 let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
1876
1877 let on_click = Function::new_with_args("cb", "this.cb = cb;");
1878 let off_click = Function::new_with_args("", "delete this.cb;");
1879 let _ = Reflect::set(&main_button, &"onClick".into(), &on_click);
1880 let _ = Reflect::set(&main_button, &"offClick".into(), &off_click);
1881
1882 let called = Rc::new(Cell::new(false));
1883 let called_clone = Rc::clone(&called);
1884
1885 let app = TelegramWebApp::instance().unwrap();
1886 let handle = app
1887 .set_bottom_button_callback(BottomButton::Main, move || {
1888 called_clone.set(true);
1889 })
1890 .unwrap();
1891
1892 let stored = Reflect::has(&main_button, &"cb".into()).unwrap();
1893 assert!(stored);
1894
1895 let cb_fn = Reflect::get(&main_button, &"cb".into())
1896 .unwrap()
1897 .dyn_into::<Function>()
1898 .unwrap();
1899 let _ = cb_fn.call0(&JsValue::NULL);
1900 assert!(called.get());
1901
1902 app.remove_bottom_button_callback(handle).unwrap();
1903 let stored_after = Reflect::has(&main_button, &"cb".into()).unwrap();
1904 assert!(!stored_after);
1905 }
1906
1907 #[wasm_bindgen_test]
1908 #[allow(dead_code, clippy::unused_unit)]
1909 fn secondary_button_callback_register_and_remove() {
1910 let webapp = setup_webapp();
1911 let secondary_button = Object::new();
1912 let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
1913
1914 let on_click = Function::new_with_args("cb", "this.cb = cb;");
1915 let off_click = Function::new_with_args("", "delete this.cb;");
1916 let _ = Reflect::set(&secondary_button, &"onClick".into(), &on_click);
1917 let _ = Reflect::set(&secondary_button, &"offClick".into(), &off_click);
1918
1919 let called = Rc::new(Cell::new(false));
1920 let called_clone = Rc::clone(&called);
1921
1922 let app = TelegramWebApp::instance().unwrap();
1923 let handle = app
1924 .set_bottom_button_callback(BottomButton::Secondary, move || {
1925 called_clone.set(true);
1926 })
1927 .unwrap();
1928
1929 let stored = Reflect::has(&secondary_button, &"cb".into()).unwrap();
1930 assert!(stored);
1931
1932 let cb_fn = Reflect::get(&secondary_button, &"cb".into())
1933 .unwrap()
1934 .dyn_into::<Function>()
1935 .unwrap();
1936 let _ = cb_fn.call0(&JsValue::NULL);
1937 assert!(called.get());
1938
1939 app.remove_bottom_button_callback(handle).unwrap();
1940 let stored_after = Reflect::has(&secondary_button, &"cb".into()).unwrap();
1941 assert!(!stored_after);
1942 }
1943
1944 #[wasm_bindgen_test]
1945 #[allow(dead_code, clippy::unused_unit)]
1946 fn on_event_register_and_remove() {
1947 let webapp = setup_webapp();
1948 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
1949 let off_event = Function::new_with_args("name", "delete this[name];");
1950 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
1951 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
1952
1953 let app = TelegramWebApp::instance().unwrap();
1954 let handle = app.on_event("test", |_: JsValue| {}).unwrap();
1955 assert!(Reflect::has(&webapp, &"test".into()).unwrap());
1956 app.off_event(handle).unwrap();
1957 assert!(!Reflect::has(&webapp, &"test".into()).unwrap());
1958 }
1959
1960 #[wasm_bindgen_test]
1961 #[allow(dead_code, clippy::unused_unit)]
1962 fn background_event_register_and_remove() {
1963 let webapp = setup_webapp();
1964 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
1965 let off_event = Function::new_with_args("name", "delete this[name];");
1966 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
1967 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
1968
1969 let app = TelegramWebApp::instance().unwrap();
1970 let handle = app
1971 .on_background_event(BackgroundEvent::MainButtonClicked, |_| {})
1972 .unwrap();
1973 assert!(Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
1974 app.off_event(handle).unwrap();
1975 assert!(!Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
1976 }
1977
1978 #[wasm_bindgen_test]
1979 #[allow(dead_code, clippy::unused_unit)]
1980 fn background_event_delivers_data() {
1981 let webapp = setup_webapp();
1982 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
1983 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
1984
1985 let app = TelegramWebApp::instance().unwrap();
1986 let received = Rc::new(RefCell::new(String::new()));
1987 let received_clone = Rc::clone(&received);
1988 let _handle = app
1989 .on_background_event(BackgroundEvent::InvoiceClosed, move |v| {
1990 *received_clone.borrow_mut() = v.as_string().unwrap_or_default();
1991 })
1992 .unwrap();
1993
1994 let cb = Reflect::get(&webapp, &"invoiceClosed".into())
1995 .unwrap()
1996 .dyn_into::<Function>()
1997 .unwrap();
1998 let _ = cb.call1(&JsValue::NULL, &JsValue::from_str("paid"));
1999 assert_eq!(received.borrow().as_str(), "paid");
2000 }
2001
2002 #[wasm_bindgen_test]
2003 #[allow(dead_code, clippy::unused_unit)]
2004 fn theme_changed_register_and_remove() {
2005 let webapp = setup_webapp();
2006 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2007 let off_event = Function::new_with_args("name", "delete this[name];");
2008 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2009 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2010
2011 let app = TelegramWebApp::instance().unwrap();
2012 let handle = app.on_theme_changed(|| {}).unwrap();
2013 assert!(Reflect::has(&webapp, &"themeChanged".into()).unwrap());
2014 app.off_event(handle).unwrap();
2015 assert!(!Reflect::has(&webapp, &"themeChanged".into()).unwrap());
2016 }
2017
2018 #[wasm_bindgen_test]
2019 #[allow(dead_code, clippy::unused_unit)]
2020 fn safe_area_changed_register_and_remove() {
2021 let webapp = setup_webapp();
2022 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2023 let off_event = Function::new_with_args("name", "delete this[name];");
2024 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2025 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2026
2027 let app = TelegramWebApp::instance().unwrap();
2028 let handle = app.on_safe_area_changed(|| {}).unwrap();
2029 assert!(Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
2030 app.off_event(handle).unwrap();
2031 assert!(!Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
2032 }
2033
2034 #[wasm_bindgen_test]
2035 #[allow(dead_code, clippy::unused_unit)]
2036 fn content_safe_area_changed_register_and_remove() {
2037 let webapp = setup_webapp();
2038 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2039 let off_event = Function::new_with_args("name", "delete this[name];");
2040 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2041 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2042
2043 let app = TelegramWebApp::instance().unwrap();
2044 let handle = app.on_content_safe_area_changed(|| {}).unwrap();
2045 assert!(Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
2046 app.off_event(handle).unwrap();
2047 assert!(!Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
2048 }
2049
2050 #[wasm_bindgen_test]
2051 #[allow(dead_code, clippy::unused_unit)]
2052 fn viewport_changed_register_and_remove() {
2053 let webapp = setup_webapp();
2054 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2055 let off_event = Function::new_with_args("name", "delete this[name];");
2056 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2057 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2058
2059 let app = TelegramWebApp::instance().unwrap();
2060 let handle = app.on_viewport_changed(|| {}).unwrap();
2061 assert!(Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
2062 app.off_event(handle).unwrap();
2063 assert!(!Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
2064 }
2065
2066 #[wasm_bindgen_test]
2067 #[allow(dead_code, clippy::unused_unit)]
2068 fn clipboard_text_received_register_and_remove() {
2069 let webapp = setup_webapp();
2070 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2071 let off_event = Function::new_with_args("name", "delete this[name];");
2072 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2073 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2074
2075 let app = TelegramWebApp::instance().unwrap();
2076 let handle = app.on_clipboard_text_received(|_| {}).unwrap();
2077 assert!(Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
2078 app.off_event(handle).unwrap();
2079 assert!(!Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
2080 }
2081
2082 #[wasm_bindgen_test]
2083 #[allow(dead_code, clippy::unused_unit)]
2084 fn open_link_and_telegram_link() {
2085 let webapp = setup_webapp();
2086 let open_link = Function::new_with_args("url", "this.open_link = url;");
2087 let open_tg_link = Function::new_with_args("url", "this.open_tg_link = url;");
2088 let _ = Reflect::set(&webapp, &"openLink".into(), &open_link);
2089 let _ = Reflect::set(&webapp, &"openTelegramLink".into(), &open_tg_link);
2090
2091 let app = TelegramWebApp::instance().unwrap();
2092 let url = "https://example.com";
2093 app.open_link(url).unwrap();
2094 app.open_telegram_link(url).unwrap();
2095
2096 assert_eq!(
2097 Reflect::get(&webapp, &"open_link".into())
2098 .unwrap()
2099 .as_string()
2100 .as_deref(),
2101 Some(url)
2102 );
2103 assert_eq!(
2104 Reflect::get(&webapp, &"open_tg_link".into())
2105 .unwrap()
2106 .as_string()
2107 .as_deref(),
2108 Some(url)
2109 );
2110 }
2111
2112 #[wasm_bindgen_test]
2113 #[allow(dead_code, clippy::unused_unit)]
2114 fn invoice_closed_register_and_remove() {
2115 let webapp = setup_webapp();
2116 let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
2117 let off_event = Function::new_with_args("name", "delete this[name];");
2118 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2119 let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
2120
2121 let app = TelegramWebApp::instance().unwrap();
2122 let handle = app.on_invoice_closed(|_| {}).unwrap();
2123 assert!(Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
2124 app.off_event(handle).unwrap();
2125 assert!(!Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
2126 }
2127
2128 #[wasm_bindgen_test]
2129 #[allow(dead_code, clippy::unused_unit)]
2130 fn invoice_closed_invokes_callback() {
2131 let webapp = setup_webapp();
2132 let on_event = Function::new_with_args("name, cb", "this.cb = cb;");
2133 let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
2134
2135 let app = TelegramWebApp::instance().unwrap();
2136 let status = Rc::new(RefCell::new(String::new()));
2137 let status_clone = Rc::clone(&status);
2138 app.on_invoice_closed(move |s| {
2139 *status_clone.borrow_mut() = s;
2140 })
2141 .unwrap();
2142
2143 let cb = Reflect::get(&webapp, &"cb".into())
2144 .unwrap()
2145 .dyn_into::<Function>()
2146 .unwrap();
2147 cb.call1(&webapp, &"paid".into()).unwrap();
2148 assert_eq!(status.borrow().as_str(), "paid");
2149 cb.call1(&webapp, &"failed".into()).unwrap();
2150 assert_eq!(status.borrow().as_str(), "failed");
2151 }
2152
2153 #[wasm_bindgen_test]
2154 #[allow(dead_code, clippy::unused_unit)]
2155 fn open_invoice_invokes_callback() {
2156 let webapp = setup_webapp();
2157 let open_invoice = Function::new_with_args("url, cb", "cb('paid');");
2158 let _ = Reflect::set(&webapp, &"openInvoice".into(), &open_invoice);
2159
2160 let app = TelegramWebApp::instance().unwrap();
2161 let status = Rc::new(RefCell::new(String::new()));
2162 let status_clone = Rc::clone(&status);
2163
2164 app.open_invoice("https://invoice", move |s| {
2165 *status_clone.borrow_mut() = s;
2166 })
2167 .unwrap();
2168
2169 assert_eq!(status.borrow().as_str(), "paid");
2170 }
2171
2172 #[wasm_bindgen_test]
2173 #[allow(dead_code, clippy::unused_unit)]
2174 fn switch_inline_query_calls_js() {
2175 let webapp = setup_webapp();
2176 let switch_inline =
2177 Function::new_with_args("query, types", "this.query = query; this.types = types;");
2178 let _ = Reflect::set(&webapp, &"switchInlineQuery".into(), &switch_inline);
2179
2180 let app = TelegramWebApp::instance().unwrap();
2181 let types = JsValue::from_str("users");
2182 app.switch_inline_query("search", Some(&types)).unwrap();
2183
2184 assert_eq!(
2185 Reflect::get(&webapp, &"query".into())
2186 .unwrap()
2187 .as_string()
2188 .as_deref(),
2189 Some("search"),
2190 );
2191 assert_eq!(
2192 Reflect::get(&webapp, &"types".into())
2193 .unwrap()
2194 .as_string()
2195 .as_deref(),
2196 Some("users"),
2197 );
2198 }
2199
2200 #[wasm_bindgen_test]
2201 #[allow(dead_code, clippy::unused_unit)]
2202 fn share_message_calls_js() {
2203 let webapp = setup_webapp();
2204 let share = Function::new_with_args("id, cb", "this.shared_id = id; cb(true);");
2205 let _ = Reflect::set(&webapp, &"shareMessage".into(), &share);
2206
2207 let app = TelegramWebApp::instance().unwrap();
2208 let sent = Rc::new(Cell::new(false));
2209 let sent_clone = Rc::clone(&sent);
2210
2211 app.share_message("123", move |s| {
2212 sent_clone.set(s);
2213 })
2214 .unwrap();
2215
2216 assert_eq!(
2217 Reflect::get(&webapp, &"shared_id".into())
2218 .unwrap()
2219 .as_string()
2220 .as_deref(),
2221 Some("123"),
2222 );
2223 assert!(sent.get());
2224 }
2225
2226 #[wasm_bindgen_test]
2227 #[allow(dead_code, clippy::unused_unit)]
2228 fn share_to_story_calls_js() {
2229 let webapp = setup_webapp();
2230 let share = Function::new_with_args(
2231 "url, params",
2232 "this.story_url = url; this.story_params = params;"
2233 );
2234 let _ = Reflect::set(&webapp, &"shareToStory".into(), &share);
2235
2236 let app = TelegramWebApp::instance().unwrap();
2237 let url = "https://example.com/media";
2238 let params = Object::new();
2239 let _ = Reflect::set(¶ms, &"text".into(), &"hi".into());
2240 app.share_to_story(url, Some(¶ms.into())).unwrap();
2241
2242 assert_eq!(
2243 Reflect::get(&webapp, &"story_url".into())
2244 .unwrap()
2245 .as_string()
2246 .as_deref(),
2247 Some(url),
2248 );
2249 let stored = Reflect::get(&webapp, &"story_params".into()).unwrap();
2250 assert_eq!(
2251 Reflect::get(&stored, &"text".into())
2252 .unwrap()
2253 .as_string()
2254 .as_deref(),
2255 Some("hi"),
2256 );
2257 }
2258
2259 #[wasm_bindgen_test]
2260 #[allow(dead_code, clippy::unused_unit)]
2261 fn share_url_calls_js() {
2262 let webapp = setup_webapp();
2263 let share = Function::new_with_args(
2264 "url, text",
2265 "this.shared_url = url; this.shared_text = text;"
2266 );
2267 let _ = Reflect::set(&webapp, &"shareURL".into(), &share);
2268
2269 let app = TelegramWebApp::instance().unwrap();
2270 let url = "https://example.com";
2271 let text = "check";
2272 app.share_url(url, Some(text)).unwrap();
2273
2274 assert_eq!(
2275 Reflect::get(&webapp, &"shared_url".into())
2276 .unwrap()
2277 .as_string()
2278 .as_deref(),
2279 Some(url),
2280 );
2281 assert_eq!(
2282 Reflect::get(&webapp, &"shared_text".into())
2283 .unwrap()
2284 .as_string()
2285 .as_deref(),
2286 Some(text),
2287 );
2288 }
2289
2290 #[wasm_bindgen_test]
2291 #[allow(dead_code, clippy::unused_unit)]
2292 fn join_voice_chat_calls_js() {
2293 let webapp = setup_webapp();
2294 let join = Function::new_with_args(
2295 "id, hash",
2296 "this.voice_chat_id = id; this.voice_chat_hash = hash;"
2297 );
2298 let _ = Reflect::set(&webapp, &"joinVoiceChat".into(), &join);
2299
2300 let app = TelegramWebApp::instance().unwrap();
2301 app.join_voice_chat("123", Some("hash")).unwrap();
2302
2303 assert_eq!(
2304 Reflect::get(&webapp, &"voice_chat_id".into())
2305 .unwrap()
2306 .as_string()
2307 .as_deref(),
2308 Some("123"),
2309 );
2310 assert_eq!(
2311 Reflect::get(&webapp, &"voice_chat_hash".into())
2312 .unwrap()
2313 .as_string()
2314 .as_deref(),
2315 Some("hash"),
2316 );
2317 }
2318
2319 #[wasm_bindgen_test]
2320 #[allow(dead_code, clippy::unused_unit)]
2321 fn add_to_home_screen_calls_js() {
2322 let webapp = setup_webapp();
2323 let add = Function::new_with_args("", "this.called = true; return true;");
2324 let _ = Reflect::set(&webapp, &"addToHomeScreen".into(), &add);
2325
2326 let app = TelegramWebApp::instance().unwrap();
2327 let shown = app.add_to_home_screen().unwrap();
2328 assert!(shown);
2329 let called = Reflect::get(&webapp, &"called".into())
2330 .unwrap()
2331 .as_bool()
2332 .unwrap_or(false);
2333 assert!(called);
2334 }
2335
2336 #[wasm_bindgen_test]
2337 #[allow(dead_code, clippy::unused_unit)]
2338 fn request_fullscreen_calls_js() {
2339 let webapp = setup_webapp();
2340 let called = Rc::new(Cell::new(false));
2341 let called_clone = Rc::clone(&called);
2342
2343 let cb = Closure::<dyn FnMut()>::new(move || {
2344 called_clone.set(true);
2345 });
2346 let _ = Reflect::set(
2347 &webapp,
2348 &"requestFullscreen".into(),
2349 cb.as_ref().unchecked_ref()
2350 );
2351 cb.forget();
2352
2353 let app = TelegramWebApp::instance().unwrap();
2354 app.request_fullscreen().unwrap();
2355 assert!(called.get());
2356 }
2357
2358 #[wasm_bindgen_test]
2359 #[allow(dead_code, clippy::unused_unit)]
2360 fn exit_fullscreen_calls_js() {
2361 let webapp = setup_webapp();
2362 let called = Rc::new(Cell::new(false));
2363 let called_clone = Rc::clone(&called);
2364
2365 let cb = Closure::<dyn FnMut()>::new(move || {
2366 called_clone.set(true);
2367 });
2368 let _ = Reflect::set(
2369 &webapp,
2370 &"exitFullscreen".into(),
2371 cb.as_ref().unchecked_ref()
2372 );
2373 cb.forget();
2374
2375 let app = TelegramWebApp::instance().unwrap();
2376 app.exit_fullscreen().unwrap();
2377 assert!(called.get());
2378 }
2379
2380 #[wasm_bindgen_test]
2381 #[allow(dead_code, clippy::unused_unit)]
2382 fn check_home_screen_status_invokes_callback() {
2383 let webapp = setup_webapp();
2384 let check = Function::new_with_args("cb", "cb('added');");
2385 let _ = Reflect::set(&webapp, &"checkHomeScreenStatus".into(), &check);
2386
2387 let app = TelegramWebApp::instance().unwrap();
2388 let status = Rc::new(RefCell::new(String::new()));
2389 let status_clone = Rc::clone(&status);
2390
2391 app.check_home_screen_status(move |s| {
2392 *status_clone.borrow_mut() = s;
2393 })
2394 .unwrap();
2395
2396 assert_eq!(status.borrow().as_str(), "added");
2397 }
2398
2399 #[wasm_bindgen_test]
2400 #[allow(dead_code, clippy::unused_unit)]
2401 fn lock_orientation_calls_js() {
2402 let webapp = setup_webapp();
2403 let received = Rc::new(RefCell::new(None));
2404 let rc_clone = Rc::clone(&received);
2405
2406 let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
2407 *rc_clone.borrow_mut() = v.as_string();
2408 });
2409 let _ = Reflect::set(
2410 &webapp,
2411 &"lockOrientation".into(),
2412 cb.as_ref().unchecked_ref()
2413 );
2414 cb.forget();
2415
2416 let app = TelegramWebApp::instance().unwrap();
2417 app.lock_orientation("portrait").unwrap();
2418 assert_eq!(received.borrow().as_deref(), Some("portrait"));
2419 }
2420
2421 #[wasm_bindgen_test]
2422 #[allow(dead_code, clippy::unused_unit)]
2423 fn unlock_orientation_calls_js() {
2424 let webapp = setup_webapp();
2425 let called = Rc::new(Cell::new(false));
2426 let called_clone = Rc::clone(&called);
2427
2428 let cb = Closure::<dyn FnMut()>::new(move || {
2429 called_clone.set(true);
2430 });
2431 let _ = Reflect::set(
2432 &webapp,
2433 &"unlockOrientation".into(),
2434 cb.as_ref().unchecked_ref()
2435 );
2436 cb.forget();
2437
2438 let app = TelegramWebApp::instance().unwrap();
2439 app.unlock_orientation().unwrap();
2440 assert!(called.get());
2441 }
2442
2443 #[wasm_bindgen_test]
2444 #[allow(dead_code, clippy::unused_unit)]
2445 fn enable_vertical_swipes_calls_js() {
2446 let webapp = setup_webapp();
2447 let called = Rc::new(Cell::new(false));
2448 let called_clone = Rc::clone(&called);
2449
2450 let cb = Closure::<dyn FnMut()>::new(move || {
2451 called_clone.set(true);
2452 });
2453 let _ = Reflect::set(
2454 &webapp,
2455 &"enableVerticalSwipes".into(),
2456 cb.as_ref().unchecked_ref()
2457 );
2458 cb.forget();
2459
2460 let app = TelegramWebApp::instance().unwrap();
2461 app.enable_vertical_swipes().unwrap();
2462 assert!(called.get());
2463 }
2464
2465 #[wasm_bindgen_test]
2466 #[allow(dead_code, clippy::unused_unit)]
2467 fn disable_vertical_swipes_calls_js() {
2468 let webapp = setup_webapp();
2469 let called = Rc::new(Cell::new(false));
2470 let called_clone = Rc::clone(&called);
2471
2472 let cb = Closure::<dyn FnMut()>::new(move || {
2473 called_clone.set(true);
2474 });
2475 let _ = Reflect::set(
2476 &webapp,
2477 &"disableVerticalSwipes".into(),
2478 cb.as_ref().unchecked_ref()
2479 );
2480 cb.forget();
2481
2482 let app = TelegramWebApp::instance().unwrap();
2483 app.disable_vertical_swipes().unwrap();
2484 assert!(called.get());
2485 }
2486
2487 #[wasm_bindgen_test]
2488 #[allow(dead_code, clippy::unused_unit)]
2489 fn request_write_access_invokes_callback() {
2490 let webapp = setup_webapp();
2491 let request = Function::new_with_args("cb", "cb(true);");
2492 let _ = Reflect::set(&webapp, &"requestWriteAccess".into(), &request);
2493
2494 let app = TelegramWebApp::instance().unwrap();
2495 let granted = Rc::new(Cell::new(false));
2496 let granted_clone = Rc::clone(&granted);
2497
2498 let res = app.request_write_access(move |g| {
2499 granted_clone.set(g);
2500 });
2501 assert!(res.is_ok());
2502
2503 assert!(granted.get());
2504 }
2505
2506 #[wasm_bindgen_test]
2507 #[allow(dead_code, clippy::unused_unit)]
2508 fn download_file_invokes_callback() {
2509 let webapp = setup_webapp();
2510 let received_url = Rc::new(RefCell::new(String::new()));
2511 let received_name = Rc::new(RefCell::new(String::new()));
2512 let url_clone = Rc::clone(&received_url);
2513 let name_clone = Rc::clone(&received_name);
2514
2515 let download = Closure::<dyn FnMut(JsValue, JsValue)>::new(move |params, cb: JsValue| {
2516 let url = Reflect::get(¶ms, &"url".into())
2517 .unwrap()
2518 .as_string()
2519 .unwrap_or_default();
2520 let name = Reflect::get(¶ms, &"file_name".into())
2521 .unwrap()
2522 .as_string()
2523 .unwrap_or_default();
2524 *url_clone.borrow_mut() = url;
2525 *name_clone.borrow_mut() = name;
2526 let func = cb.dyn_ref::<Function>().unwrap();
2527 let _ = func.call1(&JsValue::NULL, &JsValue::from_str("id"));
2528 });
2529 let _ = Reflect::set(
2530 &webapp,
2531 &"downloadFile".into(),
2532 download.as_ref().unchecked_ref()
2533 );
2534 download.forget();
2535
2536 let app = TelegramWebApp::instance().unwrap();
2537 let result = Rc::new(RefCell::new(String::new()));
2538 let result_clone = Rc::clone(&result);
2539 let params = DownloadFileParams {
2540 url: "https://example.com/data.bin",
2541 file_name: Some("data.bin"),
2542 mime_type: None
2543 };
2544 app.download_file(params, move |id| {
2545 *result_clone.borrow_mut() = id;
2546 })
2547 .unwrap();
2548
2549 assert_eq!(
2550 received_url.borrow().as_str(),
2551 "https://example.com/data.bin"
2552 );
2553 assert_eq!(received_name.borrow().as_str(), "data.bin");
2554 assert_eq!(result.borrow().as_str(), "id");
2555 }
2556
2557 #[wasm_bindgen_test]
2558 #[allow(dead_code, clippy::unused_unit)]
2559 fn request_write_access_returns_error_when_missing() {
2560 let _webapp = setup_webapp();
2561 let app = TelegramWebApp::instance().unwrap();
2562 let res = app.request_write_access(|_| {});
2563 assert!(res.is_err());
2564 }
2565 #[wasm_bindgen_test]
2566 #[allow(dead_code, clippy::unused_unit)]
2567 fn request_emoji_status_access_invokes_callback() {
2568 let webapp = setup_webapp();
2569 let request = Function::new_with_args("cb", "cb(false);");
2570 let _ = Reflect::set(&webapp, &"requestEmojiStatusAccess".into(), &request);
2571
2572 let app = TelegramWebApp::instance().unwrap();
2573 let granted = Rc::new(Cell::new(true));
2574 let granted_clone = Rc::clone(&granted);
2575
2576 app.request_emoji_status_access(move |g| {
2577 granted_clone.set(g);
2578 })
2579 .unwrap();
2580
2581 assert!(!granted.get());
2582 }
2583
2584 #[wasm_bindgen_test]
2585 #[allow(dead_code, clippy::unused_unit)]
2586 fn set_emoji_status_invokes_callback() {
2587 let webapp = setup_webapp();
2588 let set_status = Function::new_with_args("status, cb", "this.st = status; cb(true);");
2589 let _ = Reflect::set(&webapp, &"setEmojiStatus".into(), &set_status);
2590
2591 let status = Object::new();
2592 let _ = Reflect::set(
2593 &status,
2594 &"custom_emoji_id".into(),
2595 &JsValue::from_str("321")
2596 );
2597
2598 let app = TelegramWebApp::instance().unwrap();
2599 let success = Rc::new(Cell::new(false));
2600 let success_clone = Rc::clone(&success);
2601
2602 app.set_emoji_status(&status.into(), move |s| {
2603 success_clone.set(s);
2604 })
2605 .unwrap();
2606
2607 assert!(success.get());
2608 let stored = Reflect::get(&webapp, &"st".into()).unwrap();
2609 let id = Reflect::get(&stored, &"custom_emoji_id".into())
2610 .unwrap()
2611 .as_string();
2612 assert_eq!(id.as_deref(), Some("321"));
2613 }
2614
2615 #[wasm_bindgen_test]
2616 #[allow(dead_code, clippy::unused_unit)]
2617 fn show_popup_invokes_callback() {
2618 let webapp = setup_webapp();
2619 let show_popup = Function::new_with_args("params, cb", "cb('ok');");
2620 let _ = Reflect::set(&webapp, &"showPopup".into(), &show_popup);
2621
2622 let app = TelegramWebApp::instance().unwrap();
2623 let button = Rc::new(RefCell::new(String::new()));
2624 let button_clone = Rc::clone(&button);
2625
2626 app.show_popup(&JsValue::NULL, move |id| {
2627 *button_clone.borrow_mut() = id;
2628 })
2629 .unwrap();
2630
2631 assert_eq!(button.borrow().as_str(), "ok");
2632 }
2633
2634 #[wasm_bindgen_test]
2635 #[allow(dead_code, clippy::unused_unit)]
2636 fn read_text_from_clipboard_invokes_callback() {
2637 let webapp = setup_webapp();
2638 let read_clip = Function::new_with_args("cb", "cb('clip');");
2639 let _ = Reflect::set(&webapp, &"readTextFromClipboard".into(), &read_clip);
2640
2641 let app = TelegramWebApp::instance().unwrap();
2642 let text = Rc::new(RefCell::new(String::new()));
2643 let text_clone = Rc::clone(&text);
2644
2645 app.read_text_from_clipboard(move |t| {
2646 *text_clone.borrow_mut() = t;
2647 })
2648 .unwrap();
2649
2650 assert_eq!(text.borrow().as_str(), "clip");
2651 }
2652
2653 #[wasm_bindgen_test]
2654 #[allow(dead_code, clippy::unused_unit)]
2655 fn scan_qr_popup_invokes_callback_and_close() {
2656 let webapp = setup_webapp();
2657 let show_scan = Function::new_with_args("text, cb", "cb('code');");
2658 let close_scan = Function::new_with_args("", "this.closed = true;");
2659 let _ = Reflect::set(&webapp, &"showScanQrPopup".into(), &show_scan);
2660 let _ = Reflect::set(&webapp, &"closeScanQrPopup".into(), &close_scan);
2661
2662 let app = TelegramWebApp::instance().unwrap();
2663 let text = Rc::new(RefCell::new(String::new()));
2664 let text_clone = Rc::clone(&text);
2665
2666 app.show_scan_qr_popup("scan", move |value| {
2667 *text_clone.borrow_mut() = value;
2668 })
2669 .unwrap();
2670 assert_eq!(text.borrow().as_str(), "code");
2671
2672 app.close_scan_qr_popup().unwrap();
2673 let closed = Reflect::get(&webapp, &"closed".into())
2674 .unwrap()
2675 .as_bool()
2676 .unwrap_or(false);
2677 assert!(closed);
2678 }
2679}