telegram_webapp_sdk/leptos/
bottom_button.rs1use std::{cell::RefCell, collections::HashMap};
5
6use leptos::prelude::*;
7
8use crate::{
9 logger,
10 webapp::{BottomButton as WebBottomButton, EventHandle, TelegramWebApp}
11};
12
13thread_local! {
14 static BUTTON_HANDLES: RefCell<HashMap<WebBottomButton, EventHandle<dyn FnMut()>>> =
15 RefCell::new(HashMap::new());
16}
17
18#[component]
52pub fn BottomButton<F>(
53 #[prop(into)]
55 text: Signal<String>,
56 #[prop(optional, into)]
58 color: Option<Signal<String>>,
59 #[prop(optional, into)]
61 text_color: Option<Signal<String>>,
62 #[prop(optional)]
64 on_click: Option<F>,
65 #[prop(default = WebBottomButton::Main)]
67 button: WebBottomButton
68) -> impl IntoView
69where
70 F: Fn() + Clone + 'static
71{
72 Effect::new(move |_| {
74 if let Some(app) = TelegramWebApp::instance() {
75 if let Err(err) = app.show_bottom_button(button) {
76 logger::error(&format!("show_bottom_button failed: {err:?}"));
77 }
78 } else {
79 logger::error("TelegramWebApp instance not available");
80 }
81 });
82
83 Effect::new(move |_| {
85 if let Some(app) = TelegramWebApp::instance()
86 && let Err(err) = app.set_bottom_button_text(button, &text.get())
87 {
88 logger::error(&format!("set_bottom_button_text failed: {err:?}"));
89 }
90 });
91
92 if let Some(color) = color {
94 Effect::new(move |_| {
95 if let Some(app) = TelegramWebApp::instance()
96 && let Err(err) = app.set_bottom_button_color(button, &color.get())
97 {
98 logger::error(&format!("set_bottom_button_color failed: {err:?}"));
99 }
100 });
101 }
102
103 if let Some(text_color) = text_color {
105 Effect::new(move |_| {
106 if let Some(app) = TelegramWebApp::instance()
107 && let Err(err) = app.set_bottom_button_text_color(button, &text_color.get())
108 {
109 logger::error(&format!("set_bottom_button_text_color failed: {err:?}"));
110 }
111 });
112 }
113
114 if let Some(cb) = on_click {
116 if let Some(app) = TelegramWebApp::instance() {
117 match app.set_bottom_button_callback(button, cb) {
118 Ok(handle) => BUTTON_HANDLES.with(|handles| {
119 handles.borrow_mut().insert(button, handle);
120 }),
121 Err(err) => logger::error(&format!("set_bottom_button_callback failed: {err:?}"))
122 }
123 } else {
124 logger::error("TelegramWebApp instance not available");
125 }
126 }
127
128 on_cleanup(move || {
130 if let Some(app) = TelegramWebApp::instance() {
131 BUTTON_HANDLES.with(|handles| {
132 if let Some(handle) = handles.borrow_mut().remove(&button)
133 && let Err(err) = app.remove_bottom_button_callback(handle)
134 {
135 logger::error(&format!("remove_bottom_button_callback failed: {err:?}"));
136 }
137 });
138 if let Err(err) = app.hide_bottom_button(button) {
139 logger::error(&format!("hide_bottom_button failed: {err:?}"));
140 }
141 } else {
142 logger::error("TelegramWebApp instance not available");
143 }
144 });
145
146 View::new(())
148}
149
150#[cfg(all(test, feature = "leptos"))]
151mod tests {
152 use std::{
153 cell::{Cell, RefCell},
154 rc::Rc
155 };
156
157 use js_sys::{Function, Object, Reflect};
158 use leptos::prelude::*;
159 use wasm_bindgen::{JsCast, JsValue, closure::Closure};
160 use wasm_bindgen_test::wasm_bindgen_test;
161 use web_sys::window;
162
163 use super::BottomButton;
164 use crate::webapp::BottomButton as WebBottomButton;
165
166 #[allow(dead_code, clippy::type_complexity)]
167 fn setup_webapp() -> (
168 Rc<Cell<bool>>,
169 Rc<Cell<bool>>,
170 Rc<RefCell<Vec<String>>>,
171 Rc<RefCell<Option<Function>>>
172 ) {
173 let win = window().unwrap();
174 let tg = Object::new();
175 let webapp = Object::new();
176 let button = Object::new();
177
178 let show_called = Rc::new(Cell::new(false));
179 let show_cb = {
180 let show_called = Rc::clone(&show_called);
181 Closure::<dyn FnMut()>::new(move || {
182 show_called.set(true);
183 })
184 };
185 Reflect::set(&button, &"show".into(), show_cb.as_ref().unchecked_ref()).unwrap();
186 show_cb.forget();
187
188 let hide_called = Rc::new(Cell::new(false));
189 let hide_cb = {
190 let hide_called = Rc::clone(&hide_called);
191 Closure::<dyn FnMut()>::new(move || {
192 hide_called.set(true);
193 })
194 };
195 Reflect::set(&button, &"hide".into(), hide_cb.as_ref().unchecked_ref()).unwrap();
196 hide_cb.forget();
197
198 let texts = Rc::new(RefCell::new(Vec::new()));
199 let set_text_cb = {
200 let texts = Rc::clone(&texts);
201 Closure::<dyn FnMut(JsValue)>::new(move |val: JsValue| {
202 if let Some(s) = val.as_string() {
203 texts.borrow_mut().push(s);
204 }
205 })
206 };
207 Reflect::set(
208 &button,
209 &"setText".into(),
210 set_text_cb.as_ref().unchecked_ref()
211 )
212 .unwrap();
213 set_text_cb.forget();
214
215 let click_fn = Rc::new(RefCell::new(None));
216 let on_click_cb = {
217 let click_fn = Rc::clone(&click_fn);
218 Closure::<dyn Fn(JsValue)>::new(move |f: JsValue| {
219 *click_fn.borrow_mut() = f.dyn_into::<Function>().ok();
220 })
221 };
222 Reflect::set(
223 &button,
224 &"onClick".into(),
225 on_click_cb.as_ref().unchecked_ref()
226 )
227 .unwrap();
228 on_click_cb.forget();
229
230 let off_click_cb = {
231 let click_fn = Rc::clone(&click_fn);
232 Closure::<dyn FnMut(JsValue)>::new(move |_f: JsValue| {
233 *click_fn.borrow_mut() = None;
234 })
235 };
236 Reflect::set(
237 &button,
238 &"offClick".into(),
239 off_click_cb.as_ref().unchecked_ref()
240 )
241 .unwrap();
242 off_click_cb.forget();
243
244 Reflect::set(&webapp, &"MainButton".into(), &button).unwrap();
245 Reflect::set(&tg, &"WebApp".into(), &webapp).unwrap();
246 Reflect::set(&win, &"Telegram".into(), &tg).unwrap();
247
248 (show_called, hide_called, texts, click_fn)
249 }
250
251 #[wasm_bindgen_test]
252 #[allow(dead_code)]
253 fn bottom_button_updates_and_cleans_up() {
254 let (show_called, hide_called, texts, click_fn) = setup_webapp();
255
256 let owner = Owner::new();
257 owner.set();
258 let (text, set_text) = signal("Start".to_owned());
259 let clicked = Rc::new(Cell::new(false));
260 let clicked_clone = Rc::clone(&clicked);
261 let _view = view! {
262 <BottomButton
263 button=WebBottomButton::Main
264 text
265 on_click=move || clicked_clone.set(true)
266 />
267 };
268 set_text.set("Next".to_owned());
269 if let Some(func) = click_fn.borrow().as_ref() {
270 let _ = func.call0(&JsValue::NULL);
271 }
272 assert!(clicked.get());
273 drop(owner);
274
275 assert!(show_called.get());
276 assert!(hide_called.get());
277 assert!(click_fn.borrow().is_none());
278 let stored = texts.borrow();
279 assert_eq!(stored.as_slice(), ["Start", "Next"]);
280 }
281}