telegram_webapp_sdk/api/
secure_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 secure storage.
10///
11/// Values are stored in an encrypted form and can be restored after the user
12/// reinstalls the application.
13///
14/// # Errors
15/// Returns `Err(JsValue)` if the JavaScript call fails or `secureStorage` is
16/// missing.
17///
18/// # Examples
19/// ```
20/// use telegram_webapp_sdk::api::secure_storage::set;
21/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
22/// set("token", "123").await?;
23/// # Ok(()) }
24/// ```
25pub async fn set(key: &str, value: &str) -> Result<(), JsValue> {
26    let storage = secure_storage_object()?;
27    let func = Reflect::get(&storage, &JsValue::from_str("set"))?.dyn_into::<Function>()?;
28    let promise = func
29        .call2(&storage, &JsValue::from_str(key), &JsValue::from_str(value))?
30        .dyn_into::<Promise>()?;
31    JsFuture::from(promise).await?;
32    Ok(())
33}
34
35/// Retrieves a value from Telegram's secure storage.
36///
37/// # Errors
38/// Returns `Err(JsValue)` if the JavaScript call fails or `secureStorage` is
39/// missing.
40///
41/// # Examples
42/// ```
43/// use telegram_webapp_sdk::api::secure_storage::{get, set};
44/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
45/// set("token", "123").await?;
46/// let value = get("token").await?;
47/// assert_eq!(value.as_deref(), Some("123"));
48/// # Ok(()) }
49/// ```
50pub async fn get(key: &str) -> Result<Option<String>, JsValue> {
51    let storage = secure_storage_object()?;
52    let func = Reflect::get(&storage, &JsValue::from_str("get"))?.dyn_into::<Function>()?;
53    let promise = func
54        .call1(&storage, &JsValue::from_str(key))?
55        .dyn_into::<Promise>()?;
56    let value = JsFuture::from(promise).await?;
57    Ok(value.as_string())
58}
59
60/// Restores a previously removed value from Telegram's secure storage.
61///
62/// # Errors
63/// Returns `Err(JsValue)` if the JavaScript call fails or `secureStorage` is
64/// missing.
65///
66/// # Examples
67/// ```
68/// use telegram_webapp_sdk::api::secure_storage::{remove, restore, set};
69/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
70/// set("token", "123").await?;
71/// remove("token").await?;
72/// let _ = restore("token").await?;
73/// # Ok(()) }
74/// ```
75pub async fn restore(key: &str) -> Result<Option<String>, JsValue> {
76    let storage = secure_storage_object()?;
77    let func = Reflect::get(&storage, &JsValue::from_str("restore"))?.dyn_into::<Function>()?;
78    let promise = func
79        .call1(&storage, &JsValue::from_str(key))?
80        .dyn_into::<Promise>()?;
81    let value = JsFuture::from(promise).await?;
82    Ok(value.as_string())
83}
84
85/// Removes a value from Telegram's secure storage.
86///
87/// # Errors
88/// Returns `Err(JsValue)` if the JavaScript call fails or `secureStorage` is
89/// missing.
90///
91/// # Examples
92/// ```
93/// use telegram_webapp_sdk::api::secure_storage::{remove, set};
94/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
95/// set("token", "123").await?;
96/// remove("token").await?;
97/// # Ok(()) }
98/// ```
99pub async fn remove(key: &str) -> Result<(), JsValue> {
100    let storage = secure_storage_object()?;
101    let func = Reflect::get(&storage, &JsValue::from_str("remove"))?.dyn_into::<Function>()?;
102    let promise = func
103        .call1(&storage, &JsValue::from_str(key))?
104        .dyn_into::<Promise>()?;
105    JsFuture::from(promise).await?;
106    Ok(())
107}
108
109/// Clears all entries from Telegram's secure storage.
110///
111/// # Errors
112/// Returns `Err(JsValue)` if the JavaScript call fails or `secureStorage` is
113/// missing.
114///
115/// # Examples
116/// ```
117/// use telegram_webapp_sdk::api::secure_storage::{clear, set};
118/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
119/// set("token", "123").await?;
120/// clear().await?;
121/// # Ok(()) }
122/// ```
123pub async fn clear() -> Result<(), JsValue> {
124    let storage = secure_storage_object()?;
125    let func = Reflect::get(&storage, &JsValue::from_str("clear"))?.dyn_into::<Function>()?;
126    let promise = func.call0(&storage)?.dyn_into::<Promise>()?;
127    JsFuture::from(promise).await?;
128    Ok(())
129}
130
131fn secure_storage_object() -> Result<JsValue, JsValue> {
132    let window = window().ok_or_else(|| JsValue::from_str("no window"))?;
133    let tg = Reflect::get(&window, &JsValue::from_str("Telegram"))?;
134    let webapp = Reflect::get(&tg, &JsValue::from_str("WebApp"))?;
135    Reflect::get(&webapp, &JsValue::from_str("secureStorage"))
136}
137
138#[cfg(test)]
139mod tests {
140    use js_sys::{Function, Object, Reflect};
141    use wasm_bindgen::prelude::*;
142    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
143    use web_sys::window;
144
145    use super::*;
146
147    wasm_bindgen_test_configure!(run_in_browser);
148
149    #[allow(dead_code)]
150    fn setup_secure_storage() -> Object {
151        let win = window().unwrap();
152        let telegram = Object::new();
153        let webapp = Object::new();
154        let storage = Object::new();
155        let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
156        let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
157        let _ = Reflect::set(&webapp, &"secureStorage".into(), &storage);
158        storage
159    }
160
161    #[wasm_bindgen_test(async)]
162    #[allow(dead_code)]
163    async fn set_calls_js() {
164        let storage = setup_secure_storage();
165        let func = Function::new_with_args("k,v", "this[k] = v; return Promise.resolve();");
166        let _ = Reflect::set(&storage, &"set".into(), &func);
167        assert!(set("a", "b").await.is_ok());
168        let val = Reflect::get(&storage, &"a".into()).unwrap();
169        assert_eq!(val.as_string().as_deref(), Some("b"));
170    }
171
172    #[wasm_bindgen_test(async)]
173    #[allow(dead_code)]
174    async fn set_err() {
175        assert!(set("a", "b").await.is_err());
176    }
177
178    #[wasm_bindgen_test(async)]
179    #[allow(dead_code)]
180    async fn get_calls_js() {
181        let storage = setup_secure_storage();
182        let func = Function::new_with_args("k", "return this[k];");
183        let _ = Reflect::set(&storage, &"get".into(), &func);
184        let _ = Reflect::set(&storage, &"a".into(), &JsValue::from_str("b"));
185        let value = get("a").await.unwrap();
186        assert_eq!(value.as_deref(), Some("b"));
187    }
188
189    #[wasm_bindgen_test(async)]
190    #[allow(dead_code)]
191    async fn get_err() {
192        assert!(get("a").await.is_err());
193    }
194
195    #[wasm_bindgen_test(async)]
196    #[allow(dead_code)]
197    async fn restore_calls_js() {
198        let storage = setup_secure_storage();
199        let func = Function::new_with_args("k", "return this[k];");
200        let _ = Reflect::set(&storage, &"restore".into(), &func);
201        let _ = Reflect::set(&storage, &"a".into(), &JsValue::from_str("b"));
202        let value = restore("a").await.unwrap();
203        assert_eq!(value.as_deref(), Some("b"));
204    }
205
206    #[wasm_bindgen_test(async)]
207    #[allow(dead_code)]
208    async fn restore_err() {
209        assert!(restore("a").await.is_err());
210    }
211
212    #[wasm_bindgen_test(async)]
213    #[allow(dead_code)]
214    async fn remove_calls_js() {
215        let storage = setup_secure_storage();
216        let func = Function::new_with_args("k", "delete this[k]; return Promise.resolve();");
217        let _ = Reflect::set(&storage, &"remove".into(), &func);
218        let _ = Reflect::set(&storage, &"a".into(), &JsValue::from_str("b"));
219        assert!(remove("a").await.is_ok());
220        let has = Reflect::has(&storage, &"a".into()).unwrap();
221        assert!(!has);
222    }
223
224    #[wasm_bindgen_test(async)]
225    #[allow(dead_code)]
226    async fn remove_err() {
227        assert!(remove("a").await.is_err());
228    }
229
230    #[wasm_bindgen_test(async)]
231    #[allow(dead_code)]
232    async fn clear_calls_js() {
233        let storage = setup_secure_storage();
234        let func = Function::new_no_args(
235            "Object.keys(this).forEach(k => delete this[k]); return Promise.resolve();"
236        );
237        let _ = Reflect::set(&storage, &"clear".into(), &func);
238        let _ = Reflect::set(&storage, &"a".into(), &JsValue::from_str("b"));
239        assert!(clear().await.is_ok());
240        let has = Reflect::has(&storage, &"a".into()).unwrap();
241        assert!(!has);
242    }
243
244    #[wasm_bindgen_test(async)]
245    #[allow(dead_code)]
246    async fn clear_err() {
247        assert!(clear().await.is_err());
248    }
249}