telegram_webapp_sdk/core/
context.rs1use once_cell::unsync::OnceCell;
5use percent_encoding::{percent_decode, percent_decode_str};
6use wasm_bindgen::JsValue;
7
8use super::types::{
9 init_data::TelegramInitData, launch_params::LaunchParams, theme_params::TelegramThemeParams
10};
11
12#[derive(Clone)]
14pub struct TelegramContext {
15 pub init_data: TelegramInitData,
18 pub theme_params: TelegramThemeParams,
21 pub raw_init_data: String
24}
25
26thread_local! {
27 static CONTEXT: OnceCell<TelegramContext> = const { OnceCell::new() };
29}
30
31impl TelegramContext {
32 pub fn init(
37 init_data: TelegramInitData,
38 theme_params: TelegramThemeParams,
39 raw_init_data: String
40 ) -> Result<(), &'static str> {
41 CONTEXT.with(|cell| {
42 cell.set(TelegramContext {
43 init_data,
44 theme_params,
45 raw_init_data
46 })
47 .map_err(|_| "TelegramContext already initialized")
48 })
49 }
50
51 pub fn get<F, R>(f: F) -> Option<R>
55 where
56 F: FnOnce(&TelegramContext) -> R
57 {
58 CONTEXT.with(|cell| cell.get().map(f))
59 }
60
61 pub fn get_raw_init_data() -> Result<String, &'static str> {
86 Self::get(|ctx| ctx.raw_init_data.clone()).ok_or("TelegramContext not initialized")
87 }
88}
89
90pub fn get_launch_params() -> Result<LaunchParams, JsValue> {
104 let _ = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
105
106 Ok(LaunchParams {
107 tg_web_app_platform: get_param("tgWebAppPlatform")
108 .or_else(|| Some(String::from("web"))),
109 tg_web_app_version: get_param("tgWebAppVersion"),
110 tg_web_app_start_param: get_param("tgWebAppStartParam"),
111 tg_web_app_show_settings: get_param("tgWebAppShowSettings").map(|s| s == "1"),
112 tg_web_app_bot_inline: get_param("tgWebAppBotInline").map(|s| s == "1")
113 })
114}
115
116fn get_param(key: &str) -> Option<String> {
117 let search = web_sys::window()?.document()?.location()?.search().ok()?;
118
119 let query = search.strip_prefix('?').unwrap_or(search.as_str());
120 extract_param(query, key)
121}
122
123fn extract_param(query: &str, key: &str) -> Option<String> {
124 query.split('&').find_map(|pair| {
125 if pair.is_empty() {
126 return None;
127 }
128
129 let mut parts = pair.splitn(2, '=');
130 let current_key = parts.next()?;
131 if current_key != key {
132 return None;
133 }
134
135 let raw_value = parts.next()?;
136 decode_query_value(raw_value)
137 })
138}
139
140fn decode_query_value(raw_value: &str) -> Option<String> {
141 if raw_value.contains('+') {
142 let mut buffer = Vec::with_capacity(raw_value.len());
143 for byte in raw_value.as_bytes() {
144 if *byte == b'+' {
145 buffer.push(b' ');
146 } else {
147 buffer.push(*byte);
148 }
149 }
150
151 return percent_decode(buffer.as_slice())
152 .decode_utf8()
153 .ok()
154 .map(|cow| cow.into_owned());
155 }
156
157 percent_decode_str(raw_value)
158 .decode_utf8()
159 .ok()
160 .map(|cow| cow.into_owned())
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn extract_param_returns_first_entry() {
169 let query = "tgWebAppPlatform=android&tgWebAppVersion=9.2";
170 let platform = extract_param(query, "tgWebAppPlatform");
171 assert_eq!(platform.as_deref(), Some("android"));
172 }
173
174 #[test]
175 fn decode_query_value_handles_plus_and_percent_sequences() {
176 let query = "tgWebAppStartParam=hello%2Bworld+test";
177 let value = extract_param(query, "tgWebAppStartParam");
178 assert_eq!(value.as_deref(), Some("hello+world test"));
179 }
180
181 #[cfg(target_arch = "wasm32")]
182 mod wasm {
183 use wasm_bindgen::JsValue;
184 use wasm_bindgen_test::wasm_bindgen_test;
185
186 use super::super::get_launch_params;
187
188 #[allow(dead_code)]
189 #[wasm_bindgen_test]
190 fn get_launch_params_returns_error_without_window() {
191 let err = get_launch_params().unwrap_err();
192 assert_eq!(err, JsValue::from_str("no window"));
193 }
194
195 #[wasm_bindgen_test]
196 fn get_launch_params_reads_first_query_parameter() -> Result<(), JsValue> {
197 let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
198 let location = window.location();
199 let original_search = location.search().unwrap_or_default();
200
201 location.set_search(
202 "?tgWebAppPlatform=android&tgWebAppVersion=9.2&tgWebAppStartParam=hello%2Bworld+test&tgWebAppShowSettings=1&tgWebAppBotInline=0"
203 )?;
204
205 let params = get_launch_params()?;
206 assert_eq!(params.tg_web_app_platform.as_deref(), Some("android"));
207 assert_eq!(params.tg_web_app_version.as_deref(), Some("9.2"));
208 assert_eq!(
209 params.tg_web_app_start_param.as_deref(),
210 Some("hello+world test")
211 );
212 assert_eq!(params.tg_web_app_show_settings, Some(true));
213 assert_eq!(params.tg_web_app_bot_inline, Some(false));
214
215 location.set_search(&original_search)?;
216 Ok(())
217 }
218 }
219}