-
-
Notifications
You must be signed in to change notification settings - Fork 0
3. Core Concepts
This chapter explains the fundamental concepts you need to understand to build Telegram Mini Apps with telegram-webapp-sdk.
When your WebApp loads, the SDK must initialize before you can use any Telegram features. The initialization process:
1. Browser loads your HTML + WASM
↓
2. Telegram script creates window.Telegram.WebApp
↓
3. init_sdk() reads Telegram data
↓
4. SDK creates global TelegramContext
↓
5. Your app can now use Telegram APIs
Use the #[telegram_app(auto_init)] macro:
use telegram_webapp_sdk::telegram_app;
use yew::prelude::*;
#[telegram_app(auto_init)]
#[function_component(App)]
fn app() -> Html {
html! {
<h1>{ "Hello Telegram!" }</h1>
}
}The macro wraps your component and calls init_sdk() before rendering.
Call init_sdk() or try_init_sdk() explicitly:
use telegram_webapp_sdk::core::init::{init_sdk, try_init_sdk};
// Panics if initialization fails
fn main() {
init_sdk().expect("Failed to initialize SDK");
// ... your app code
}
// Graceful error handling
fn main() {
match try_init_sdk() {
Ok(true) => {
println!("SDK initialized");
// ... your app code
}
Ok(false) => {
println!("Not in Telegram environment");
// ... fallback behavior
}
Err(e) => {
eprintln!("Initialization error: {}", e);
}
}
}The SDK provides typed errors for debugging:
use telegram_webapp_sdk::core::init::{try_init_sdk, InitError};
match try_init_sdk() {
Ok(_) => { /* success */ }
Err(InitError::WindowUnavailable) => {
eprintln!("Browser window not available");
}
Err(InitError::TelegramUnavailable) => {
eprintln!("window.Telegram is undefined - did you include the Telegram script?");
}
Err(InitError::WebAppUnavailable) => {
eprintln!("Telegram.WebApp is undefined");
}
Err(InitError::InitDataParseFailed(msg)) => {
eprintln!("Failed to parse init data: {}", msg);
}
Err(InitError::ThemeParamsParseFailed(msg)) => {
eprintln!("Failed to parse theme: {}", msg);
}
Err(InitError::ContextInitFailed(msg)) => {
eprintln!("Context initialization failed: {}", msg);
}
}The SDK maintains a global context that holds all Telegram-provided data:
pub struct TelegramContext {
pub init_data: TelegramInitData, // User, chat, auth data
pub theme_params: TelegramThemeParams, // Color scheme
pub raw_init_data: String, // For server validation
}This context is initialized once and stored in thread-local storage:
┌─────────────────────────────────────────┐
│ Your WebApp Process │
│ │
│ ┌────────────────────────────────────┐ │
│ │ Thread-Local Storage │ │
│ │ │ │
│ │ TelegramContext (OnceCell) │ │
│ │ ├── init_data │ │
│ │ ├── theme_params │ │
│ │ └── raw_init_data │ │
│ └────────────────────────────────────┘ │
│ │
│ All components access same context │
└─────────────────────────────────────────┘
use telegram_webapp_sdk::core::context::TelegramContext;
// Access context with a closure
let user_id = TelegramContext::get(|ctx| {
ctx.init_data.user.as_ref().map(|u| u.id)
});
match user_id {
Some(Some(id)) => println!("User ID: {}", id),
Some(None) => println!("No user data"),
None => println!("Context not initialized"),
}The raw_init_data field contains the URL-encoded string from Telegram. Send this to your backend for validation:
use telegram_webapp_sdk::core::context::TelegramContext;
match TelegramContext::get_raw_init_data() {
Ok(raw) => {
// Send to backend
send_to_server(&raw).await;
}
Err(e) => eprintln!("Error: {}", e),
}The init_data contains information about the user, chat, and session:
pub struct TelegramInitData {
pub query_id: Option<String>, // Query ID for answerWebAppQuery
pub user: Option<TelegramUser>, // Current user
pub receiver: Option<TelegramUser>, // Receiver in private chat
pub chat: Option<TelegramChat>, // Current chat
pub chat_type: Option<String>, // Chat type: sender, private, group, supergroup, channel
pub chat_instance: Option<String>, // Chat instance ID
pub start_param: Option<String>, // Deep link parameter
pub can_send_after: Option<i64>, // Timestamp when user can send messages
pub auth_date: i64, // Authentication date (Unix timestamp)
pub hash: String, // Data hash for validation
pub signature: Option<String>, // Optional signature
}User information from Telegram:
pub struct TelegramUser {
pub id: i64, // Unique user ID
pub is_bot: Option<bool>, // Is this user a bot?
pub first_name: String, // First name (always present)
pub last_name: Option<String>, // Last name
pub username: Option<String>, // Username (@username)
pub language_code: Option<String>, // IETF language tag (e.g., "en", "ru")
pub is_premium: Option<bool>, // Is Telegram Premium subscriber?
pub added_to_attachment_menu: Option<bool>, // Added bot to attachment menu?
pub allows_write_to_pm: Option<bool>, // Allows writing to PM?
pub photo_url: Option<String>, // Profile photo URL
}Chat information (when app is opened in a chat context):
pub struct TelegramChat {
pub id: i64, // Unique chat ID
pub r#type: String, // Type: group, supergroup, channel
pub title: String, // Chat title
pub username: Option<String>, // Username (@channel)
pub photo_url: Option<String>, // Chat photo URL
}use telegram_webapp_sdk::TelegramWebApp;
let webapp = TelegramWebApp::new();
let init_data = webapp.init_data_unsafe();
if let Some(user) = &init_data.user {
println!("User ID: {}", user.id);
println!("Name: {}", user.first_name);
if let Some(username) = &user.username {
println!("Username: @{}", username);
}
if let Some(is_premium) = user.is_premium {
if is_premium {
println!("Premium user!");
}
}
}Important: init_data_unsafe() returns unvalidated data. Always validate on your backend:
// Client-side: Send raw data to backend
let raw = TelegramContext::get_raw_init_data()?;
send_to_backend(&raw).await;
// Server-side: Validate with init-data-rs
use init_data_rs::{validate, InitData};
let init_data: InitData = validate(&raw, bot_token, Some(3600))?;
// Now you can trust the dataTelegram provides its color scheme so your app can match the user's theme:
pub struct TelegramThemeParams {
pub bg_color: Option<String>, // Background color (#RRGGBB)
pub text_color: Option<String>, // Text color
pub hint_color: Option<String>, // Hint text color
pub link_color: Option<String>, // Link color
pub button_color: Option<String>, // Button background
pub button_text_color: Option<String>, // Button text
pub secondary_bg_color: Option<String>, // Secondary background
pub header_bg_color: Option<String>, // Header background
pub accent_text_color: Option<String>, // Accent text
pub section_bg_color: Option<String>, // Section background
pub section_header_text_color: Option<String>, // Section header text
pub subtitle_text_color: Option<String>, // Subtitle text
pub destructive_text_color: Option<String>, // Destructive action text
}use telegram_webapp_sdk::TelegramWebApp;
let webapp = TelegramWebApp::new();
// Access theme from context
let theme = TelegramContext::get(|ctx| ctx.theme_params.clone());
if let Some(theme_params) = theme {
if let Some(bg_color) = theme_params.bg_color {
// Apply background color
set_background_color(&bg_color);
}
if let Some(button_color) = theme_params.button_color {
// Style buttons
style_button(&button_color);
}
}Apply theme colors to CSS variables:
use telegram_webapp_sdk::core::types::theme_params::TelegramThemeParams;
impl TelegramThemeParams {
pub fn into_css_vars(&self) -> Vec<(String, String)> {
let mut vars = Vec::new();
if let Some(ref bg_color) = self.bg_color {
vars.push(("--tg-theme-bg-color".to_string(), bg_color.clone()));
}
if let Some(ref text_color) = self.text_color {
vars.push(("--tg-theme-text-color".to_string(), text_color.clone()));
}
// ... all other colors
vars
}
}Then in your CSS:
body {
background-color: var(--tg-theme-bg-color, #ffffff);
color: var(--tg-theme-text-color, #000000);
}
button {
background-color: var(--tg-theme-button-color, #0088cc);
color: var(--tg-theme-button-text-color, #ffffff);
}Launch parameters come from URL query parameters and tell you how the app was launched:
pub struct LaunchParams {
pub tg_web_app_platform: Option<String>, // Platform: android, ios, web, etc.
pub tg_web_app_version: Option<String>, // Telegram version
pub tg_web_app_start_param: Option<String>, // Deep link parameter
pub tg_web_app_show_settings: Option<bool>, // Show settings button?
pub tg_web_app_bot_inline: Option<bool>, // Opened from inline mode?
}use telegram_webapp_sdk::core::context::get_launch_params;
match get_launch_params() {
Ok(params) => {
if let Some(platform) = params.tg_web_app_platform {
println!("Platform: {}", platform); // "android", "ios", "web"
}
if let Some(version) = params.tg_web_app_version {
println!("Telegram version: {}", version); // "7.0", "7.1", etc.
}
if let Some(start_param) = params.tg_web_app_start_param {
// Deep link parameter from bot link:
// https://t.me/yourbot?start=PARAM
println!("Start parameter: {}", start_param);
}
if let Some(true) = params.tg_web_app_show_settings {
// Show settings UI
}
}
Err(e) => eprintln!("Failed to get launch params: {:?}", e),
}use telegram_webapp_sdk::core::context::get_launch_params;
let params = get_launch_params()?;
match params.tg_web_app_platform.as_deref() {
Some("ios") => {
// iOS-specific behavior
println!("Running on iOS");
}
Some("android") => {
// Android-specific behavior
println!("Running on Android");
}
Some("web") | Some("weba") => {
// Web version (desktop or mobile browser)
println!("Running in browser");
}
Some("tdesktop") => {
// Telegram Desktop
println!("Running in Telegram Desktop");
}
Some("macos") => {
// macOS app
println!("Running on macOS");
}
_ => {
println!("Unknown platform");
}
}Use start_param for deep linking into specific app sections:
// In your bot, send a link like:
// https://t.me/yourbot/app?startapp=product_123
let params = get_launch_params()?;
if let Some(start_param) = params.tg_web_app_start_param {
match start_param.as_str() {
s if s.starts_with("product_") => {
let product_id = s.strip_prefix("product_").unwrap();
navigate_to_product(product_id);
}
"settings" => {
navigate_to_settings();
}
_ => {
navigate_to_home();
}
}
}TelegramWebApp is the primary interface to all Telegram features:
use telegram_webapp_sdk::TelegramWebApp;
let webapp = TelegramWebApp::new();// Lifecycle
webapp.ready(); // Tell Telegram the app is ready
webapp.close(); // Close the WebApp
webapp.expand_viewport(); // Expand to full height
// UI
webapp.set_header_color("#FF0000");
webapp.set_background_color("#FFFFFF");
webapp.set_bottom_bar_color("#000000");
// Dialogs
webapp.show_alert("Hello!");
webapp.show_confirm("Are you sure?", callback);
webapp.show_popup(params);
// Buttons
webapp.main_button().set_text("Submit");
webapp.main_button().show();
webapp.main_button().on_click(callback);
// Data
let init_data = webapp.init_data_unsafe();
let theme = webapp.theme_params();
// Navigation
webapp.open_link("https://example.com");
webapp.open_telegram_link("https://t.me/telegram");use telegram_webapp_sdk::{telegram_app, TelegramWebApp};
use yew::prelude::*;
#[telegram_app(auto_init)]
#[function_component(App)]
fn app() -> Html {
let webapp = TelegramWebApp::new();
// 1. Tell Telegram we're ready
use_effect_with((), move |_| {
webapp.ready();
webapp.expand_viewport();
|| ()
});
// 2. Set up main button
use_effect_with((), move |_| {
webapp.main_button().set_text("Submit Order");
webapp.main_button().show();
webapp.main_button().on_click(Callback::from(move |_| {
webapp.show_alert("Order submitted!");
webapp.close();
}));
|| ()
});
// 3. Apply theme
use_effect_with((), move |_| {
if let Some(bg_color) = webapp.theme_params().bg_color {
webapp.set_background_color(&bg_color);
}
|| ()
});
// 4. Render UI
html! {
<div>
<h1>{ "My Telegram App" }</h1>
{
if let Some(user) = &webapp.init_data_unsafe().user {
html! {
<p>{ format!("Hello, {}!", user.first_name) }</p>
}
} else {
html! { <p>{ "Loading..." }</p> }
}
}
</div>
}
}Rust's type system prevents entire classes of runtime errors:
// Compiler guarantees user.id is always i64
let user_id: i64 = webapp.init_data_unsafe().user.unwrap().id;
// Optional fields are explicit - won't compile without handling None
let username: Option<String> = webapp.init_data_unsafe().user.unwrap().username;
match username {
Some(name) => println!("Username: @{}", name),
None => println!("No username"),
}// Safe unwrapping with defaults
let user_name = webapp
.init_data_unsafe()
.user
.as_ref()
.map(|u| u.first_name.clone())
.unwrap_or_else(|| "Guest".to_string());
// Chaining Options
let is_premium = webapp
.init_data_unsafe()
.user
.as_ref()
.and_then(|u| u.is_premium)
.unwrap_or(false);
// Pattern matching
match &webapp.init_data_unsafe().user {
Some(user) if user.is_premium == Some(true) => {
println!("Premium user: {}", user.first_name);
}
Some(user) => {
println!("Regular user: {}", user.first_name);
}
None => {
println!("No user data");
}
}Now that you understand the core concepts, learn how to integrate with your framework:
Or explore the complete API: