telegram_webapp_sdk/core/
init.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use js_sys::Reflect;
5use serde_wasm_bindgen::from_value;
6use wasm_bindgen::JsValue;
7use web_sys::window;
8
9use crate::core::{
10    context::TelegramContext,
11    types::{
12        chat::TelegramChat, init_data::TelegramInitData,
13        init_data_internal::TelegramInitDataInternal, theme_params::TelegramThemeParams,
14        user::TelegramUser
15    }
16};
17
18/// Typed initialization errors for better error handling and debugging.
19#[derive(Debug, Clone, PartialEq)]
20pub enum InitError {
21    /// Browser `window` object is not available
22    WindowUnavailable,
23    /// `window.Telegram` is undefined
24    TelegramUnavailable,
25    /// `Telegram.WebApp` is undefined
26    WebAppUnavailable,
27    /// Failed to parse `WebApp.initData`
28    InitDataParseFailed(String),
29    /// Failed to parse theme parameters
30    ThemeParamsParseFailed(String),
31    /// Failed to initialize global context
32    ContextInitFailed(String)
33}
34
35impl std::fmt::Display for InitError {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            Self::WindowUnavailable => write!(f, "Browser window object is not available"),
39            Self::TelegramUnavailable => write!(f, "window.Telegram is undefined"),
40            Self::WebAppUnavailable => write!(f, "Telegram.WebApp is undefined"),
41            Self::InitDataParseFailed(msg) => write!(f, "Failed to parse initData: {msg}"),
42            Self::ThemeParamsParseFailed(msg) => {
43                write!(f, "Failed to parse theme parameters: {msg}")
44            }
45            Self::ContextInitFailed(msg) => write!(f, "Failed to initialize context: {msg}")
46        }
47    }
48}
49
50impl std::error::Error for InitError {}
51
52impl From<InitError> for JsValue {
53    fn from(err: InitError) -> Self {
54        JsValue::from_str(&err.to_string())
55    }
56}
57
58/// Check if Telegram WebApp environment is available.
59///
60/// Returns `true` if `window.Telegram.WebApp` exists and is defined.
61///
62/// # Examples
63/// ```no_run
64/// use telegram_webapp_sdk::core::init::is_telegram_available;
65///
66/// if is_telegram_available() {
67///     println!("Running inside Telegram Mini App");
68/// } else {
69///     println!("Running in regular browser");
70/// }
71/// ```
72pub fn is_telegram_available() -> bool {
73    window()
74        .and_then(|w| Reflect::get(&w, &"Telegram".into()).ok())
75        .filter(|tg| !tg.is_undefined())
76        .and_then(|tg| Reflect::get(&tg, &"WebApp".into()).ok())
77        .filter(|webapp| !webapp.is_undefined())
78        .is_some()
79}
80
81/// Attempt to initialize SDK without panicking if Telegram environment is
82/// unavailable.
83///
84/// Returns:
85/// - `Ok(true)` if SDK was successfully initialized
86/// - `Ok(false)` if Telegram environment is not available (graceful
87///   degradation)
88/// - `Err(InitError)` for actual initialization failures
89///
90/// # Examples
91/// ```no_run
92/// use telegram_webapp_sdk::core::init::try_init_sdk;
93///
94/// match try_init_sdk() {
95///     Ok(true) => println!("SDK initialized successfully"),
96///     Ok(false) => println!("Not running in Telegram, using fallback"),
97///     Err(e) => eprintln!("Initialization error: {}", e)
98/// }
99/// ```
100///
101/// # Errors
102/// Returns typed `InitError` for parsing failures or context initialization
103/// issues.
104pub fn try_init_sdk() -> Result<bool, InitError> {
105    if !is_telegram_available() {
106        return Ok(false);
107    }
108    init_sdk_typed().map(|_| true)
109}
110
111/// Internal typed version of init_sdk for use by try_init_sdk.
112fn init_sdk_typed() -> Result<(), InitError> {
113    let win = window().ok_or(InitError::WindowUnavailable)?;
114    let telegram =
115        Reflect::get(&win, &"Telegram".into()).map_err(|_| InitError::TelegramUnavailable)?;
116
117    if telegram.is_undefined() {
118        return Err(InitError::TelegramUnavailable);
119    }
120
121    let webapp =
122        Reflect::get(&telegram, &"WebApp".into()).map_err(|_| InitError::WebAppUnavailable)?;
123
124    if webapp.is_undefined() {
125        return Err(InitError::WebAppUnavailable);
126    }
127
128    // === 1. Parse initData string ===
129    let init_data_str = Reflect::get(&webapp, &"initData".into())
130        .ok()
131        .and_then(|v| v.as_string())
132        .ok_or_else(|| InitError::InitDataParseFailed("initData is not a string".to_string()))?;
133
134    let raw: TelegramInitDataInternal = serde_urlencoded::from_str(&init_data_str)
135        .map_err(|e| InitError::InitDataParseFailed(e.to_string()))?;
136
137    // === 2. Parse embedded JSON fields ===
138    let user: Option<TelegramUser> = raw
139        .user
140        .as_deref()
141        .map(serde_json::from_str)
142        .transpose()
143        .map_err(|e| InitError::InitDataParseFailed(format!("Failed to parse user: {e}")))?;
144
145    let receiver: Option<TelegramUser> = raw
146        .receiver
147        .as_deref()
148        .map(serde_json::from_str)
149        .transpose()
150        .map_err(|e| InitError::InitDataParseFailed(format!("Failed to parse receiver: {e}")))?;
151
152    let chat: Option<TelegramChat> = raw
153        .chat
154        .as_deref()
155        .map(serde_json::from_str)
156        .transpose()
157        .map_err(|e| InitError::InitDataParseFailed(format!("Failed to parse chat: {e}")))?;
158
159    // === 3. Construct final typed initData ===
160    let init_data = TelegramInitData {
161        query_id: raw.query_id,
162        user,
163        receiver,
164        chat,
165        chat_type: raw.chat_type,
166        chat_instance: raw.chat_instance,
167        start_param: raw.start_param,
168        can_send_after: raw.can_send_after,
169        auth_date: raw.auth_date,
170        hash: raw.hash,
171        signature: raw.signature
172    };
173
174    // === 4. Parse themeParams ===
175    let theme_val = Reflect::get(&webapp, &"themeParams".into())
176        .map_err(|e| InitError::ThemeParamsParseFailed(format!("{e:?}")))?;
177    let theme_params: TelegramThemeParams =
178        from_value(theme_val).map_err(|e| InitError::ThemeParamsParseFailed(format!("{e:?}")))?;
179
180    // === 5. Init global context ===
181    TelegramContext::init(init_data, theme_params, init_data_str)
182        .map_err(|e| InitError::ContextInitFailed(format!("{e:?}")))?;
183
184    Ok(())
185}
186
187/// Initializes Telegram WebApp SDK by extracting and validating context.
188///
189/// - Parses `initData` (urlencoded) with embedded JSON.
190/// - Parses `themeParams` (object).
191/// - Initializes global context.
192///
193/// # Errors
194///
195/// Returns `Err(JsValue)` in the following cases:
196///
197/// - `WindowUnavailable`: No browser `window` object found
198/// - `TelegramUnavailable`: `window.Telegram` is undefined
199/// - `WebAppUnavailable`: `Telegram.WebApp` is undefined
200/// - `InitDataParseFailed`: Failed to parse `WebApp.initData`
201/// - `ThemeParamsParseFailed`: Failed to parse theme parameters
202/// - `ContextInitFailed`: Failed to initialize global context
203///
204/// # Examples
205/// ```no_run
206/// use telegram_webapp_sdk::core::init::init_sdk;
207///
208/// match init_sdk() {
209///     Ok(_) => println!("SDK initialized successfully"),
210///     Err(e) => eprintln!("Initialization failed: {:?}", e)
211/// }
212/// ```
213///
214/// For better error handling, consider using [`try_init_sdk`] which returns
215/// typed [`InitError`].
216pub fn init_sdk() -> Result<(), JsValue> {
217    init_sdk_typed().map_err(Into::into)
218}