Skip to content

Commit 33be30d

Browse files
authored
#145 feat: improve initialization API with environment detection and typed errors (#152)
- Add InitError enum for typed error handling - Implement is_telegram_available() for environment detection - Add try_init_sdk() for graceful initialization with fallback - Update init_sdk() to use typed errors internally while maintaining backward compatibility - Add comprehensive tests for new functionality - Improve documentation with detailed error descriptions and usage examples
1 parent 2d4d118 commit 33be30d

File tree

2 files changed

+244
-23
lines changed

2 files changed

+244
-23
lines changed

src/core/init.rs

Lines changed: 153 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,49 +15,146 @@ use crate::core::{
1515
}
1616
};
1717

18-
/// Initializes Telegram WebApp SDK by extracting and validating context.
18+
/// Typed initialization errors for better error handling and debugging.
19+
#[derive(Debug, Clone, PartialEq)]
20+
pub 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+
35+
impl 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+
50+
impl std::error::Error for InitError {}
51+
52+
impl 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.
1959
///
20-
/// - Parses `initData` (urlencoded) with embedded JSON.
21-
/// - Parses `themeParams` (object).
22-
/// - Initializes global context.
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+
/// ```
72+
pub 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+
/// ```
23100
///
24101
/// # Errors
25-
/// Returns `Err(JsValue)` on failure to access JS globals, parse, or init
26-
/// context.
27-
pub fn init_sdk() -> Result<(), JsValue> {
28-
let win = window().ok_or_else(|| JsValue::from_str("window is not available"))?;
29-
let telegram = Reflect::get(&win, &"Telegram".into())?;
30-
let webapp = Reflect::get(&telegram, &"WebApp".into())?;
102+
/// Returns typed `InitError` for parsing failures or context initialization
103+
/// issues.
104+
pub 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.
112+
fn 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+
}
31127

32128
// === 1. Parse initData string ===
33-
let init_data_str = Reflect::get(&webapp, &"initData".into())?
34-
.as_string()
35-
.ok_or_else(|| JsValue::from_str("Telegram.WebApp.initData is not a 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()))?;
36133

37134
let raw: TelegramInitDataInternal = serde_urlencoded::from_str(&init_data_str)
38-
.map_err(|e| JsValue::from_str(&format!("Failed to parse initData: {e}")))?;
135+
.map_err(|e| InitError::InitDataParseFailed(e.to_string()))?;
39136

40137
// === 2. Parse embedded JSON fields ===
41138
let user: Option<TelegramUser> = raw
42139
.user
43140
.as_deref()
44141
.map(serde_json::from_str)
45142
.transpose()
46-
.map_err(|e| JsValue::from_str(&format!("Failed to parse user: {e}")))?;
143+
.map_err(|e| InitError::InitDataParseFailed(format!("Failed to parse user: {e}")))?;
47144

48145
let receiver: Option<TelegramUser> = raw
49146
.receiver
50147
.as_deref()
51148
.map(serde_json::from_str)
52149
.transpose()
53-
.map_err(|e| JsValue::from_str(&format!("Failed to parse receiver: {e}")))?;
150+
.map_err(|e| InitError::InitDataParseFailed(format!("Failed to parse receiver: {e}")))?;
54151

55152
let chat: Option<TelegramChat> = raw
56153
.chat
57154
.as_deref()
58155
.map(serde_json::from_str)
59156
.transpose()
60-
.map_err(|e| JsValue::from_str(&format!("Failed to parse chat: {e}")))?;
157+
.map_err(|e| InitError::InitDataParseFailed(format!("Failed to parse chat: {e}")))?;
61158

62159
// === 3. Construct final typed initData ===
63160
let init_data = TelegramInitData {
@@ -75,13 +172,47 @@ pub fn init_sdk() -> Result<(), JsValue> {
75172
};
76173

77174
// === 4. Parse themeParams ===
78-
let theme_val = Reflect::get(&webapp, &"themeParams".into())?;
79-
let theme_params: TelegramThemeParams = from_value(theme_val)?;
80-
81-
// theme_params.clone().apply_to_root();
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:?}")))?;
82179

83180
// === 5. Init global context ===
84-
TelegramContext::init(init_data, theme_params, init_data_str)?;
181+
TelegramContext::init(init_data, theme_params, init_data_str)
182+
.map_err(|e| InitError::ContextInitFailed(format!("{e:?}")))?;
85183

86184
Ok(())
87185
}
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`].
216+
pub fn init_sdk() -> Result<(), JsValue> {
217+
init_sdk_typed().map_err(Into::into)
218+
}

tests/init_sdk.rs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
use js_sys::{Object, Reflect};
77
use telegram_webapp_sdk::{
88
TelegramWebApp,
9-
core::{context::TelegramContext, init::init_sdk}
9+
core::{
10+
context::TelegramContext,
11+
init::{InitError, init_sdk, is_telegram_available, try_init_sdk}
12+
}
1013
};
1114
use wasm_bindgen::JsValue;
1215
use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
@@ -161,3 +164,90 @@ fn webapp_get_raw_init_data_suitable_for_validation() -> Result<(), JsValue> {
161164

162165
Ok(())
163166
}
167+
168+
// === Tests for new environment detection API ===
169+
170+
#[wasm_bindgen_test]
171+
fn is_telegram_available_returns_true_when_installed() -> Result<(), JsValue> {
172+
install_webapp("auth_date=1&hash=abc")?;
173+
174+
assert!(is_telegram_available());
175+
176+
Ok(())
177+
}
178+
179+
#[wasm_bindgen_test]
180+
fn is_telegram_available_returns_false_when_not_installed() -> Result<(), JsValue> {
181+
let win = window().ok_or_else(|| JsValue::from_str("no window"))?;
182+
183+
// Remove Telegram if it exists
184+
Reflect::delete_property(&win, &"Telegram".into())?;
185+
186+
assert!(!is_telegram_available());
187+
188+
Ok(())
189+
}
190+
191+
#[wasm_bindgen_test]
192+
fn try_init_sdk_returns_true_when_successful() -> Result<(), JsValue> {
193+
install_webapp("auth_date=1&hash=abc")?;
194+
195+
let result = try_init_sdk().map_err(|e| JsValue::from_str(&e.to_string()))?;
196+
197+
assert_eq!(result, true);
198+
199+
Ok(())
200+
}
201+
202+
#[wasm_bindgen_test]
203+
fn try_init_sdk_returns_false_when_telegram_unavailable() -> Result<(), JsValue> {
204+
let win = window().ok_or_else(|| JsValue::from_str("no window"))?;
205+
206+
// Remove Telegram if it exists
207+
Reflect::delete_property(&win, &"Telegram".into())?;
208+
209+
let result = try_init_sdk().map_err(|e| JsValue::from_str(&e.to_string()))?;
210+
211+
assert_eq!(result, false);
212+
213+
Ok(())
214+
}
215+
216+
#[wasm_bindgen_test]
217+
fn init_error_display_formatting() {
218+
assert_eq!(
219+
InitError::WindowUnavailable.to_string(),
220+
"Browser window object is not available"
221+
);
222+
assert_eq!(
223+
InitError::TelegramUnavailable.to_string(),
224+
"window.Telegram is undefined"
225+
);
226+
assert_eq!(
227+
InitError::WebAppUnavailable.to_string(),
228+
"Telegram.WebApp is undefined"
229+
);
230+
assert_eq!(
231+
InitError::InitDataParseFailed("test error".to_string()).to_string(),
232+
"Failed to parse initData: test error"
233+
);
234+
assert_eq!(
235+
InitError::ThemeParamsParseFailed("theme error".to_string()).to_string(),
236+
"Failed to parse theme parameters: theme error"
237+
);
238+
assert_eq!(
239+
InitError::ContextInitFailed("context error".to_string()).to_string(),
240+
"Failed to initialize context: context error"
241+
);
242+
}
243+
244+
#[wasm_bindgen_test]
245+
fn init_error_converts_to_jsvalue() {
246+
let err = InitError::WindowUnavailable;
247+
let js_val: JsValue = err.into();
248+
249+
assert_eq!(
250+
js_val.as_string().unwrap(),
251+
"Browser window object is not available"
252+
);
253+
}

0 commit comments

Comments
 (0)