telegram_webapp_sdk/api/
device_storage.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use js_sys::{Function, Promise, Reflect};
5use wasm_bindgen::{JsCast, JsValue};
6use wasm_bindgen_futures::JsFuture;
7use web_sys::window;
8
9/// Stores a value under the given key in Telegram's device storage.
10///
11/// # Errors
12/// Returns `Err(JsValue)` if the JavaScript call fails or `deviceStorage` is
13/// missing.
14///
15/// # Examples
16/// ```
17/// use telegram_webapp_sdk::api::device_storage::set;
18/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
19/// set("foo", "bar").await?;
20/// # Ok(()) }
21/// ```
22pub async fn set(key: &str, value: &str) -> Result<(), JsValue> {
23    let storage = device_storage_object()?;
24    let func = Reflect::get(&storage, &JsValue::from_str("set"))?.dyn_into::<Function>()?;
25    let promise = func
26        .call2(&storage, &JsValue::from_str(key), &JsValue::from_str(value))?
27        .dyn_into::<Promise>()?;
28    JsFuture::from(promise).await?;
29    Ok(())
30}
31
32/// Retrieves a value from Telegram's device storage.
33///
34/// # Errors
35/// Returns `Err(JsValue)` if the JavaScript call fails or `deviceStorage` is
36/// missing.
37///
38/// # Examples
39/// ```
40/// use telegram_webapp_sdk::api::device_storage::{get, set};
41/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
42/// set("foo", "bar").await?;
43/// let value = get("foo").await?;
44/// assert_eq!(value.as_deref(), Some("bar"));
45/// # Ok(()) }
46/// ```
47pub async fn get(key: &str) -> Result<Option<String>, JsValue> {
48    let storage = device_storage_object()?;
49    let func = Reflect::get(&storage, &JsValue::from_str("get"))?.dyn_into::<Function>()?;
50    let promise = func
51        .call1(&storage, &JsValue::from_str(key))?
52        .dyn_into::<Promise>()?;
53    let value = JsFuture::from(promise).await?;
54    Ok(value.as_string())
55}
56
57/// Removes a value from Telegram's device storage.
58///
59/// # Errors
60/// Returns `Err(JsValue)` if the JavaScript call fails or `deviceStorage` is
61/// missing.
62///
63/// # Examples
64/// ```
65/// use telegram_webapp_sdk::api::device_storage::{remove, set};
66/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
67/// set("foo", "bar").await?;
68/// remove("foo").await?;
69/// # Ok(()) }
70/// ```
71pub async fn remove(key: &str) -> Result<(), JsValue> {
72    let storage = device_storage_object()?;
73    let func = Reflect::get(&storage, &JsValue::from_str("remove"))?.dyn_into::<Function>()?;
74    let promise = func
75        .call1(&storage, &JsValue::from_str(key))?
76        .dyn_into::<Promise>()?;
77    JsFuture::from(promise).await?;
78    Ok(())
79}
80
81/// Clears all entries from Telegram's device storage.
82///
83/// # Errors
84/// Returns `Err(JsValue)` if the JavaScript call fails or `deviceStorage` is
85/// missing.
86///
87/// # Examples
88/// ```
89/// use telegram_webapp_sdk::api::device_storage::{clear, set};
90/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
91/// set("foo", "bar").await?;
92/// clear().await?;
93/// # Ok(()) }
94/// ```
95pub async fn clear() -> Result<(), JsValue> {
96    let storage = device_storage_object()?;
97    let func = Reflect::get(&storage, &JsValue::from_str("clear"))?.dyn_into::<Function>()?;
98    let promise = func.call0(&storage)?.dyn_into::<Promise>()?;
99    JsFuture::from(promise).await?;
100    Ok(())
101}
102
103fn device_storage_object() -> Result<JsValue, JsValue> {
104    let window = window().ok_or_else(|| JsValue::from_str("no window"))?;
105    let tg = Reflect::get(&window, &JsValue::from_str("Telegram"))?;
106    let webapp = Reflect::get(&tg, &JsValue::from_str("WebApp"))?;
107    Reflect::get(&webapp, &JsValue::from_str("deviceStorage"))
108}
109
110#[cfg(test)]
111mod tests {
112    use js_sys::{Function, Object, Reflect};
113    use wasm_bindgen::prelude::*;
114    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
115    use web_sys::window;
116
117    use super::*;
118
119    wasm_bindgen_test_configure!(run_in_browser);
120
121    #[allow(dead_code)]
122    fn setup_device_storage() -> Object {
123        let win = window().unwrap();
124        let telegram = Object::new();
125        let webapp = Object::new();
126        let storage = Object::new();
127        let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
128        let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
129        let _ = Reflect::set(&webapp, &"deviceStorage".into(), &storage);
130        storage
131    }
132
133    #[wasm_bindgen_test(async)]
134    #[allow(dead_code)]
135    async fn set_calls_js() {
136        let storage = setup_device_storage();
137        let func = Function::new_with_args("k,v", "this[k] = v; return Promise.resolve();");
138        let _ = Reflect::set(&storage, &"set".into(), &func);
139        assert!(set("a", "b").await.is_ok());
140        let val = Reflect::get(&storage, &"a".into()).unwrap();
141        assert_eq!(val.as_string().as_deref(), Some("b"));
142    }
143
144    #[wasm_bindgen_test(async)]
145    #[allow(dead_code)]
146    async fn set_err() {
147        assert!(set("a", "b").await.is_err());
148    }
149
150    #[wasm_bindgen_test(async)]
151    #[allow(dead_code)]
152    async fn get_calls_js() {
153        let storage = setup_device_storage();
154        let func = Function::new_with_args("k", "return this[k];");
155        let _ = Reflect::set(&storage, &"get".into(), &func);
156        let _ = Reflect::set(&storage, &"a".into(), &JsValue::from_str("b"));
157        let value = get("a").await.unwrap();
158        assert_eq!(value.as_deref(), Some("b"));
159    }
160
161    #[wasm_bindgen_test(async)]
162    #[allow(dead_code)]
163    async fn get_err() {
164        assert!(get("a").await.is_err());
165    }
166
167    #[wasm_bindgen_test(async)]
168    #[allow(dead_code)]
169    async fn remove_calls_js() {
170        let storage = setup_device_storage();
171        let func = Function::new_with_args("k", "delete this[k]; return Promise.resolve();");
172        let _ = Reflect::set(&storage, &"remove".into(), &func);
173        let _ = Reflect::set(&storage, &"a".into(), &JsValue::from_str("b"));
174        assert!(remove("a").await.is_ok());
175        let has = Reflect::has(&storage, &"a".into()).unwrap();
176        assert!(!has);
177    }
178
179    #[wasm_bindgen_test(async)]
180    #[allow(dead_code)]
181    async fn remove_err() {
182        assert!(remove("a").await.is_err());
183    }
184
185    #[wasm_bindgen_test(async)]
186    #[allow(dead_code)]
187    async fn clear_calls_js() {
188        let storage = setup_device_storage();
189        let func = Function::new_no_args(
190            "Object.keys(this).forEach(k => delete this[k]); return Promise.resolve();"
191        );
192        let _ = Reflect::set(&storage, &"clear".into(), &func);
193        let _ = Reflect::set(&storage, &"a".into(), &JsValue::from_str("b"));
194        assert!(clear().await.is_ok());
195        let has = Reflect::has(&storage, &"a".into()).unwrap();
196        assert!(!has);
197    }
198
199    #[wasm_bindgen_test(async)]
200    #[allow(dead_code)]
201    async fn clear_err() {
202        assert!(clear().await.is_err());
203    }
204}