telegram_webapp_sdk/webapp/types.rs
1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use js_sys::{Function, Object, Reflect};
5use serde::Serialize;
6use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
7
8use crate::logger;
9
10/// Handle returned when registering callbacks.
11///
12/// Automatically unregisters the callback when dropped, implementing RAII
13/// cleanup pattern to prevent memory leaks.
14///
15/// # Examples
16///
17/// ```no_run
18/// use telegram_webapp_sdk::TelegramWebApp;
19///
20/// if let Some(app) = TelegramWebApp::instance() {
21/// // Handle is automatically cleaned up when scope ends
22/// let handle = app
23/// .on_theme_changed(|| {
24/// println!("Theme changed!");
25/// })
26/// .expect("subscribe");
27///
28/// // No manual cleanup needed - Drop handles it
29/// } // <- handle dropped here, callback unregistered automatically
30/// ```
31pub struct EventHandle<T: ?Sized> {
32 pub(super) target: Object,
33 pub(super) method: &'static str,
34 pub(super) event: Option<String>,
35 pub(super) callback: Closure<T>,
36 pub(super) unregistered: bool
37}
38
39impl<T: ?Sized> EventHandle<T> {
40 pub(super) fn new(
41 target: Object,
42 method: &'static str,
43 event: Option<String>,
44 callback: Closure<T>
45 ) -> Self {
46 Self {
47 target,
48 method,
49 event,
50 callback,
51 unregistered: false
52 }
53 }
54
55 pub(crate) fn unregister(mut self) -> Result<(), JsValue> {
56 if self.unregistered {
57 return Ok(());
58 }
59
60 let f = Reflect::get(&self.target, &self.method.into())?;
61 let func = f
62 .dyn_ref::<Function>()
63 .ok_or_else(|| JsValue::from_str(&format!("{} is not a function", self.method)))?;
64 match &self.event {
65 Some(event) => func.call2(
66 &self.target,
67 &event.clone().into(),
68 self.callback.as_ref().unchecked_ref()
69 )?,
70 None => func.call1(&self.target, self.callback.as_ref().unchecked_ref())?
71 };
72
73 self.unregistered = true;
74 Ok(())
75 }
76}
77
78impl<T: ?Sized> Drop for EventHandle<T> {
79 /// Automatically unregisters the event callback when the handle is dropped.
80 ///
81 /// This implements the RAII pattern, ensuring that event handlers are
82 /// properly cleaned up even if the user forgets to manually unregister.
83 /// Errors during unregistration are logged but do not panic.
84 fn drop(&mut self) {
85 if self.unregistered {
86 return;
87 }
88
89 let f = match Reflect::get(&self.target, &self.method.into()) {
90 Ok(f) => f,
91 Err(_) => {
92 logger::error("Failed to get unregister method");
93 return;
94 }
95 };
96
97 let func = match f.dyn_ref::<Function>() {
98 Some(func) => func,
99 None => {
100 logger::error(&format!("{} is not a function", self.method));
101 return;
102 }
103 };
104
105 let result = match &self.event {
106 Some(event) => func.call2(
107 &self.target,
108 &event.clone().into(),
109 self.callback.as_ref().unchecked_ref()
110 ),
111 None => func.call1(&self.target, self.callback.as_ref().unchecked_ref())
112 };
113
114 if result.is_err() {
115 logger::error("Failed to unregister event callback");
116 }
117
118 self.unregistered = true;
119 }
120}
121
122/// Identifies which bottom button to operate on.
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
124pub enum BottomButton {
125 /// Primary bottom button.
126 Main,
127 /// Secondary bottom button.
128 Secondary
129}
130
131impl BottomButton {
132 pub(super) const fn js_name(self) -> &'static str {
133 match self {
134 BottomButton::Main => "MainButton",
135 BottomButton::Secondary => "SecondaryButton"
136 }
137 }
138}
139
140/// Position of the secondary bottom button.
141///
142/// # Examples
143/// ```no_run
144/// use telegram_webapp_sdk::webapp::{SecondaryButtonPosition, TelegramWebApp};
145///
146/// if let Some(app) = TelegramWebApp::instance() {
147/// match app.secondary_button_position() {
148/// Some(SecondaryButtonPosition::Top) => {}
149/// _ => {}
150/// }
151/// }
152/// ```
153#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
154#[serde(rename_all = "lowercase")]
155pub enum SecondaryButtonPosition {
156 /// Displayed above the main button.
157 Top,
158 /// Displayed to the left of the main button.
159 Left,
160 /// Displayed below the main button.
161 Bottom,
162 /// Displayed to the right of the main button.
163 Right
164}
165
166impl SecondaryButtonPosition {
167 pub(super) fn from_js_value(value: JsValue) -> Option<Self> {
168 let as_str = value.as_string()?;
169 match as_str.as_str() {
170 "top" => Some(Self::Top),
171 "left" => Some(Self::Left),
172 "bottom" => Some(Self::Bottom),
173 "right" => Some(Self::Right),
174 _ => None
175 }
176 }
177}
178
179/// Safe area insets reported by Telegram.
180///
181/// # Examples
182/// ```no_run
183/// use telegram_webapp_sdk::webapp::{SafeAreaInset, TelegramWebApp};
184///
185/// if let Some(app) = TelegramWebApp::instance() {
186/// if let Some(SafeAreaInset {
187/// top,
188/// bottom,
189/// ..
190/// }) = app.safe_area_inset()
191/// {
192/// let _ = (top, bottom);
193/// }
194/// }
195/// ```
196#[derive(Clone, Copy, Debug, PartialEq)]
197pub struct SafeAreaInset {
198 /// Distance from the top edge in CSS pixels.
199 pub top: f64,
200 /// Distance from the bottom edge in CSS pixels.
201 pub bottom: f64,
202 /// Distance from the left edge in CSS pixels.
203 pub left: f64,
204 /// Distance from the right edge in CSS pixels.
205 pub right: f64
206}
207
208impl SafeAreaInset {
209 pub(super) fn from_js(value: JsValue) -> Option<Self> {
210 let object = value.dyn_into::<Object>().ok()?;
211 let top = Reflect::get(&object, &"top".into()).ok()?.as_f64()?;
212 let bottom = Reflect::get(&object, &"bottom".into()).ok()?.as_f64()?;
213 let left = Reflect::get(&object, &"left".into()).ok()?.as_f64()?;
214 let right = Reflect::get(&object, &"right".into()).ok()?.as_f64()?;
215 Some(Self {
216 top,
217 bottom,
218 left,
219 right
220 })
221 }
222}
223
224/// Parameters accepted by bottom buttons when updating state via `setParams`.
225///
226/// # Examples
227/// ```no_run
228/// use telegram_webapp_sdk::webapp::{BottomButton, BottomButtonParams, TelegramWebApp};
229///
230/// if let Some(app) = TelegramWebApp::instance() {
231/// let params = BottomButtonParams {
232/// text: Some("Send"),
233/// is_active: Some(true),
234/// ..Default::default()
235/// };
236/// let _ = app.set_bottom_button_params(BottomButton::Main, ¶ms);
237/// }
238/// ```
239#[derive(Debug, Default, Serialize)]
240pub struct BottomButtonParams<'a> {
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub text: Option<&'a str>,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub color: Option<&'a str>,
245 #[serde(skip_serializing_if = "Option::is_none")]
246 pub text_color: Option<&'a str>,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub is_active: Option<bool>,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub is_visible: Option<bool>,
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub has_shine_effect: Option<bool>,
253 /// Custom emoji ID for the button icon (Bot API 9.5+).
254 ///
255 /// # Examples
256 /// ```no_run
257 /// use telegram_webapp_sdk::webapp::{BottomButton, BottomButtonParams, TelegramWebApp};
258 ///
259 /// if let Some(app) = TelegramWebApp::instance() {
260 /// let params = BottomButtonParams {
261 /// text: Some("Send"),
262 /// icon_custom_emoji_id: Some("123456789"),
263 /// ..Default::default()
264 /// };
265 /// let _ = app.set_bottom_button_params(BottomButton::Main, ¶ms);
266 /// }
267 /// ```
268 #[serde(
269 skip_serializing_if = "Option::is_none",
270 rename = "icon_custom_emoji_id"
271 )]
272 pub icon_custom_emoji_id: Option<&'a str>
273}
274
275/// Additional parameters supported by the secondary button.
276///
277/// # Examples
278/// ```no_run
279/// use telegram_webapp_sdk::webapp::{
280/// SecondaryButtonParams, SecondaryButtonPosition, TelegramWebApp
281/// };
282///
283/// if let Some(app) = TelegramWebApp::instance() {
284/// let params = SecondaryButtonParams {
285/// common: Default::default(),
286/// position: Some(SecondaryButtonPosition::Top)
287/// };
288/// let _ = app.set_secondary_button_params(¶ms);
289/// }
290/// ```
291#[derive(Debug, Default, Serialize)]
292pub struct SecondaryButtonParams<'a> {
293 #[serde(flatten)]
294 pub common: BottomButtonParams<'a>,
295 #[serde(skip_serializing_if = "Option::is_none")]
296 pub position: Option<SecondaryButtonPosition>
297}
298
299/// Options supported by [`crate::webapp::TelegramWebApp::open_link`].
300///
301/// # Examples
302/// ```no_run
303/// use telegram_webapp_sdk::webapp::{OpenLinkOptions, TelegramWebApp};
304///
305/// if let Some(app) = TelegramWebApp::instance() {
306/// let options = OpenLinkOptions {
307/// try_instant_view: Some(true)
308/// };
309/// let _ = app.open_link("https://example.com", Some(&options));
310/// }
311/// ```
312#[derive(Debug, Default, Serialize)]
313pub struct OpenLinkOptions {
314 #[serde(skip_serializing_if = "Option::is_none")]
315 pub try_instant_view: Option<bool>
316}
317
318/// Background events delivered by Telegram when the Mini App runs in the
319/// background.
320#[derive(Clone, Copy, Debug)]
321pub enum BackgroundEvent {
322 /// The main button was clicked. Payload: [`JsValue::UNDEFINED`].
323 MainButtonClicked,
324 /// The back button was clicked. Payload: [`JsValue::UNDEFINED`].
325 BackButtonClicked,
326 /// The settings button was clicked. Payload: [`JsValue::UNDEFINED`].
327 SettingsButtonClicked,
328 /// User responded to a write access request. Payload: `bool`.
329 WriteAccessRequested,
330 /// User responded to a contact request. Payload: `bool`.
331 ContactRequested,
332 /// User responded to a phone number request. Payload: `bool`.
333 PhoneRequested,
334 /// An invoice was closed. Payload: status string.
335 InvoiceClosed,
336 /// A popup was closed. Payload: object containing `button_id`.
337 PopupClosed,
338 /// Text was received from the QR scanner. Payload: scanned text.
339 QrTextReceived,
340 /// Text was read from the clipboard. Payload: clipboard text.
341 ClipboardTextReceived
342}
343
344impl BackgroundEvent {
345 pub(super) const fn as_str(self) -> &'static str {
346 match self {
347 BackgroundEvent::MainButtonClicked => "mainButtonClicked",
348 BackgroundEvent::BackButtonClicked => "backButtonClicked",
349 BackgroundEvent::SettingsButtonClicked => "settingsButtonClicked",
350 BackgroundEvent::WriteAccessRequested => "writeAccessRequested",
351 BackgroundEvent::ContactRequested => "contactRequested",
352 BackgroundEvent::PhoneRequested => "phoneRequested",
353 BackgroundEvent::InvoiceClosed => "invoiceClosed",
354 BackgroundEvent::PopupClosed => "popupClosed",
355 BackgroundEvent::QrTextReceived => "qrTextReceived",
356 BackgroundEvent::ClipboardTextReceived => "clipboardTextReceived"
357 }
358 }
359}