telegram_webapp_sdk/api/
haptic.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use js_sys::{Function, Reflect};
5use wasm_bindgen::{JsCast, prelude::*};
6use web_sys::window;
7
8/// Available styles for [`impact_occurred`].
9#[derive(Debug, Clone, Copy)]
10pub enum HapticImpactStyle {
11    /// A light impact feedback.
12    Light,
13    /// A medium impact feedback.
14    Medium,
15    /// A heavy impact feedback.
16    Heavy,
17    /// A rigid impact feedback.
18    Rigid,
19    /// A soft impact feedback.
20    Soft
21}
22
23impl HapticImpactStyle {
24    const fn as_str(self) -> &'static str {
25        match self {
26            Self::Light => "light",
27            Self::Medium => "medium",
28            Self::Heavy => "heavy",
29            Self::Rigid => "rigid",
30            Self::Soft => "soft"
31        }
32    }
33}
34
35/// Available types for [`notification_occurred`].
36#[derive(Debug, Clone, Copy)]
37pub enum HapticNotificationType {
38    /// Error notification feedback.
39    Error,
40    /// Success notification feedback.
41    Success,
42    /// Warning notification feedback.
43    Warning
44}
45
46impl HapticNotificationType {
47    const fn as_str(self) -> &'static str {
48        match self {
49            Self::Error => "error",
50            Self::Success => "success",
51            Self::Warning => "warning"
52        }
53    }
54}
55
56/// Triggers a haptic impact feedback.
57///
58/// # Errors
59/// Returns `Err(JsValue)` if the JavaScript call fails or `HapticFeedback` is
60/// missing.
61///
62/// # Examples
63/// ```
64/// use telegram_webapp_sdk::api::haptic::{HapticImpactStyle, impact_occurred};
65/// # fn run() -> Result<(), wasm_bindgen::JsValue> {
66/// impact_occurred(HapticImpactStyle::Light)?;
67/// # Ok(()) }
68/// ```
69pub fn impact_occurred(style: HapticImpactStyle) -> Result<(), JsValue> {
70    let haptic = haptic_object()?;
71    let func = Reflect::get(&haptic, &"impactOccurred".into())?.dyn_into::<Function>()?;
72    func.call1(&haptic, &JsValue::from_str(style.as_str()))?;
73    Ok(())
74}
75
76/// Triggers a haptic notification feedback.
77///
78/// # Errors
79/// Returns `Err(JsValue)` if the JavaScript call fails or `HapticFeedback` is
80/// missing.
81///
82/// # Examples
83/// ```
84/// use telegram_webapp_sdk::api::haptic::{HapticNotificationType, notification_occurred};
85/// # fn run() -> Result<(), wasm_bindgen::JsValue> {
86/// notification_occurred(HapticNotificationType::Success)?;
87/// # Ok(()) }
88/// ```
89pub fn notification_occurred(ty: HapticNotificationType) -> Result<(), JsValue> {
90    let haptic = haptic_object()?;
91    let func = Reflect::get(&haptic, &"notificationOccurred".into())?.dyn_into::<Function>()?;
92    func.call1(&haptic, &JsValue::from_str(ty.as_str()))?;
93    Ok(())
94}
95
96/// Triggers a haptic selection change feedback.
97///
98/// # Errors
99/// Returns `Err(JsValue)` if the JavaScript call fails or `HapticFeedback` is
100/// missing.
101///
102/// # Examples
103/// ```
104/// use telegram_webapp_sdk::api::haptic::selection_changed;
105/// # fn run() -> Result<(), wasm_bindgen::JsValue> {
106/// selection_changed()?;
107/// # Ok(()) }
108/// ```
109pub fn selection_changed() -> Result<(), JsValue> {
110    let haptic = haptic_object()?;
111    let func = Reflect::get(&haptic, &"selectionChanged".into())?.dyn_into::<Function>()?;
112    func.call0(&haptic)?;
113    Ok(())
114}
115
116/// Internal helper to get `Telegram.WebApp.HapticFeedback` object.
117fn haptic_object() -> Result<JsValue, JsValue> {
118    let window = window().ok_or_else(|| JsValue::from_str("no window"))?;
119    let tg = Reflect::get(&window, &"Telegram".into())?;
120    let webapp = Reflect::get(&tg, &"WebApp".into())?;
121    Reflect::get(&webapp, &"HapticFeedback".into())
122}
123
124#[cfg(test)]
125mod tests {
126    use js_sys::{Object, Reflect};
127    use wasm_bindgen::prelude::*;
128    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
129    use web_sys::window;
130
131    use super::*;
132
133    wasm_bindgen_test_configure!(run_in_browser);
134
135    #[allow(dead_code)]
136    fn setup_haptic() -> Object {
137        let win = window().unwrap();
138        let telegram = Object::new();
139        let webapp = Object::new();
140        let haptic = Object::new();
141        let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
142        let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
143        let _ = Reflect::set(&webapp, &"HapticFeedback".into(), &haptic);
144        haptic
145    }
146
147    #[wasm_bindgen_test]
148    #[allow(dead_code)]
149    fn impact_calls_js() {
150        let haptic = setup_haptic();
151        let _ = Reflect::set(&haptic, &"impact_called".into(), &JsValue::FALSE);
152        let haptic_clone = haptic.clone();
153        let closure = Closure::wrap(Box::new(move |_style: JsValue| {
154            let _ = Reflect::set(&haptic_clone, &"impact_called".into(), &JsValue::TRUE);
155        }) as Box<dyn FnMut(JsValue)>);
156        let _ = Reflect::set(&haptic, &"impactOccurred".into(), closure.as_ref());
157        closure.forget();
158        let _ = impact_occurred(HapticImpactStyle::Light);
159        let flag = Reflect::get(&haptic, &"impact_called".into()).unwrap();
160        assert!(flag.as_bool().unwrap());
161    }
162
163    #[wasm_bindgen_test]
164    #[allow(dead_code)]
165    fn notification_calls_js() {
166        let haptic = setup_haptic();
167        let _ = Reflect::set(&haptic, &"notification_called".into(), &JsValue::FALSE);
168        let haptic_clone = haptic.clone();
169        let closure = Closure::wrap(Box::new(move |_ty: JsValue| {
170            let _ = Reflect::set(&haptic_clone, &"notification_called".into(), &JsValue::TRUE);
171        }) as Box<dyn FnMut(JsValue)>);
172        let _ = Reflect::set(&haptic, &"notificationOccurred".into(), closure.as_ref());
173        closure.forget();
174        let _ = notification_occurred(HapticNotificationType::Error);
175        let flag = Reflect::get(&haptic, &"notification_called".into()).unwrap();
176        assert!(flag.as_bool().unwrap());
177    }
178
179    #[wasm_bindgen_test]
180    #[allow(dead_code)]
181    fn selection_calls_js() {
182        let haptic = setup_haptic();
183        let _ = Reflect::set(&haptic, &"selection_called".into(), &JsValue::FALSE);
184        let haptic_clone = haptic.clone();
185        let closure = Closure::wrap(Box::new(move || {
186            let _ = Reflect::set(&haptic_clone, &"selection_called".into(), &JsValue::TRUE);
187        }) as Box<dyn FnMut()>);
188        let _ = Reflect::set(&haptic, &"selectionChanged".into(), closure.as_ref());
189        closure.forget();
190        let _ = selection_changed();
191        let flag = Reflect::get(&haptic, &"selection_called".into()).unwrap();
192        assert!(flag.as_bool().unwrap());
193    }
194}