Skip to content

10. Demo & Examples Deep Dive

RAprogramm edited this page Nov 4, 2025 · 1 revision

Chapter 10: Demo & Examples - Complete Deep Dive

Comprehensive guide to understanding the telegram-webapp-sdk demo application and bot backend examples. This chapter explains every component, every page, every function, and shows you exactly how everything works together.

Table of Contents

  1. Architecture Overview
  2. Demo Application Structure
  3. Application Entry Point
  4. Routing System
  5. Pages Explained
  6. Components Explained
  7. Bot Backend Integration
  8. Complete Data Flow
  9. Running and Testing
  10. Troubleshooting

Architecture Overview

The telegram-webapp-sdk demo demonstrates a complete Telegram Mini App with both frontend (WebApp) and backend (Bot) components.

System Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Telegram Ecosystem                        │
│                                                               │
│  ┌────────────────┐         ┌──────────────────┐             │
│  │  Telegram User │         │   Your Server    │             │
│  │                │         │                  │             │
│  │  ┌──────────┐  │  HTTPS  │  ┌────────────┐  │             │
│  │  │ WebApp   │──┼────────►│  │  Bot API   │  │             │
│  │  │ (Rust/   │  │  Data   │  │  Handler   │  │             │
│  │  │  WASM)   │  │         │  │ (teloxide) │  │             │
│  │  └──────────┘  │         │  └────────────┘  │             │
│  │                │         │                  │             │
│  │  Telegram App  │         │   Rust Backend   │             │
│  └────────────────┘         └──────────────────┘             │
│                                                               │
└─────────────────────────────────────────────────────────────┘

Technology Stack

Frontend (WebApp)

  • Rust with WASM compilation
  • telegram-webapp-sdk for Telegram integration
  • web-sys for DOM manipulation
  • wasm-bindgen for JavaScript interop
  • Trunk for building and bundling

Backend (Bot)

  • Rust with tokio async runtime
  • teloxide for Telegram Bot API
  • masterror for error handling
  • serde for JSON serialization
  • tracing for structured logging

Demo Application Structure

Project Layout

demo/
├── src/
│   ├── main.rs                    # Application entry point
│   ├── router.rs                  # Client-side routing logic
│   ├── components.rs              # Component module exports
│   ├── pages.rs                   # Page module exports
│   ├── components/
│   │   ├── page_layout.rs         # Page layout wrapper
│   │   ├── display_data.rs        # Key-value display component
│   │   ├── nav_link.rs            # Navigation link component
│   │   ├── rgb.rs                 # Color swatch component
│   │   └── dev_menu.rs            # Development menu controls
│   └── pages/
│       ├── index.rs               # Home page with navigation
│       ├── burger_king.rs         # Order demo with send_data()
│       ├── init_data.rs           # User/chat information
│       ├── launch_params.rs       # Platform and version info
│       └── theme_params.rs        # Telegram color scheme
├── index.html                     # HTML entry point
├── style.css                      # Application styles
├── Cargo.toml                     # Rust dependencies
├── Trunk.toml                     # Build configuration
└── README.md                      # Documentation

Dependencies Explained

Cargo.toml

[package]
name = "demo"
version = "0.2.6"
edition = "2024"
rust-version.workspace = true

[dependencies]
# Core WebAssembly bindings
wasm-bindgen = "0.2"

# Web APIs (Window, Document, HtmlElement, etc.)
web-sys = { version = "0.3", features = [
  "Window",           # window object
  "Document",         # document object
  "HtmlElement",      # HTML elements
  "HtmlImageElement", # <img> elements
  "History",          # history.pushState
  "Location",         # window.location
  "Event",            # DOM events
  "EventTarget",      # addEventListener
  "CustomEvent",      # Custom events
  "CustomEventInit",  # Event initialization
  "console",          # console.log
  "Text",             # Text nodes
] }

# Telegram WebApp SDK with mock and macros
telegram-webapp-sdk = { path = "../", features = ["mock", "macros"] }

# Error handling
masterror = { workspace = true }

# Page registry system
inventory = { workspace = true }

Key Features Enabled:

  • mock - Enables testing without Telegram
  • macros - Provides telegram_app!, telegram_page!, telegram_button! macros

Application Entry Point

src/main.rs

Purpose: Initialize the Telegram WebApp and start the router.

Complete Code Breakdown:

#![no_main]
  • Tells Rust not to use standard main function
  • Required for WebAssembly applications
  • Entry point is controlled by wasm-bindgen
pub mod components;
pub mod pages;
pub mod router;
  • Declare module structure
  • Makes components and pages accessible
  • Router handles navigation
use components::dev_menu::setup_dev_menu;
use telegram_webapp_sdk::{telegram_app, telegram_router};
use wasm_bindgen::prelude::*;
  • Import dev menu setup function
  • Import SDK macros
  • Import wasm-bindgen for WASM interop
telegram_app!(
    pub fn main() -> Result<(), JsValue> {
        setup_dev_menu();
        telegram_router!();
        Ok(())
    }
);

What telegram_app! does:

  1. Creates actual #[wasm_bindgen(start)] function
  2. Initializes Telegram WebApp context
  3. Parses launch parameters from URL
  4. Stores context in thread-local storage
  5. Calls your main function

What telegram_router! does:

  1. Collects all pages registered with telegram_page! macro
  2. Builds routing table from page paths
  3. Renders initial route based on current URL
  4. Sets up popstate listener for back/forward buttons

Initialization Flow:

Browser loads WASM
       ↓
telegram_app! expands to:
       ↓
#[wasm_bindgen(start)]
fn __telegram_app_start() {
    // Parse tgWebAppData from URL
    // Initialize context
    // Call your main()
}
       ↓
setup_dev_menu()
       ↓
telegram_router!()
       ↓
Router renders current page
       ↓
Application ready

Routing System

src/router.rs

Purpose: Client-side routing without full page reloads.

Complete Implementation Explained:

use std::collections::HashMap;
use telegram_webapp_sdk::pages::Page;
use wasm_bindgen::prelude::*;
use web_sys::{Event, EventTarget, window};

type RenderFn = fn();

Data Structures:

#[derive(Default)]
pub struct Router {
    routes: HashMap<String, RenderFn>
}
  • routes maps path strings to render functions
  • Example: "/burger-king"render_burger_king_page

Building the Router:

pub fn from_pages(pages: impl Iterator<Item = &'static Page>) -> Self {
    let mut router = Self::new();
    for page in pages {
        router = router.register(page.path, page.handler);
    }
    router
}

How telegram_router! works:

  1. inventory::iter::<Page>() collects all pages
  2. Each telegram_page! registers a Page struct:
    Page {
        path: "/burger-king",
        handler: render_burger_king_page,
    }
  3. Router builds HashMap of all routes

Navigation Implementation:

pub fn navigate(path: &str) {
    if let Some(w) = window()
        && let Ok(history) = w.history()
    {
        // Push new state
        let _ = history.push_state_with_url(&JsValue::NULL, "", Some(path));

        // Manually trigger popstate event
        if let Ok(event) = web_sys::CustomEvent::new("popstate") {
            let _ = w.dispatch_event(&event);
        }
    }
}

Why manually dispatch popstate?

  • history.pushState doesn't trigger popstate automatically
  • We need to manually fire the event to render new route
  • Browser back/forward buttons trigger popstate naturally

Event Listener Setup:

pub fn start(&self) {
    self.render_current();

    if let Some(w) = window() {
        let closure = Closure::<dyn FnMut(_)>::new({
            let router = self.routes.clone();
            move |_event: Event| {
                if let Some(path) = current_path()
                    && let Some(page) = router.get(&path)
                {
                    page();  // Call render function
                }
            }
        });

        let target: EventTarget = w.into();
        target.add_event_listener_with_callback(
            "popstate",
            closure.as_ref().unchecked_ref()
        ).unwrap();

        closure.forget();  // Keep listener alive forever
    }
}

Important: closure.forget()

  • JavaScript needs the closure to stay alive
  • Rust would normally drop it after function ends
  • forget() leaks the memory intentionally
  • This is safe for application-lifetime listeners

Pages Explained

1. Home Page (src/pages/index.rs)

Purpose: Landing page with navigation to all features.

Key Code Sections:

telegram_page!(
    "/",
    pub fn render_index_page() {
        clear_app_root();
        let page = PageLayout::new("Telegram WebApp SDK Demo");
        // ... build navigation
    }
);

What telegram_page! macro does:

// Expands to:
inventory::submit! {
    &telegram_webapp_sdk::pages::Page {
        path: "/",
        handler: render_index_page,
    }
}

Logo Display:

let logo = telegram_image!(
    document,
    "https://telegram.org/img/t_logo.png",
    class = "logo",
    alt = "Telegram logo"
).unwrap();
page.append(logo.as_ref());

telegram_image! macro expansion:

// Expands to:
{
    let img = document
        .create_element("img")?
        .dyn_into::<HtmlImageElement>()?;
    img.set_src("https://telegram.org/img/t_logo.png");
    img.set_class_name("logo");
    img.set_alt("Telegram logo");
    Ok(img)
}

Navigation Links:

page.append(&nav_link(
    "Burger King Demo",
    Some("Order burgers via Telegram"),
    "/burger-king"
));

Creates a clickable link that navigates to /burger-king route.


2. Burger King Demo (src/pages/burger_king.rs)

Purpose: Demonstrates sending data from WebApp to bot.

Complete Flow Explanation:

Step 1: Define Menu Item Structure

#[derive(Clone, Debug, PartialEq)]
struct MenuItem {
    id:          u32,
    name:        &'static str,
    price_cents: u32
}

impl MenuItem {
    fn payload(&self) -> String {
        format!(
            r#"{{"id":{},"name":"{}","price_cents":{}}}"#,
            self.id, self.name, self.price_cents
        )
    }
}

Why JSON string format?

  • send_data() sends string to bot
  • Bot expects JSON format
  • Manual formatting avoids serde dependency
  • Ensures exact format bot expects

Step 2: Render Menu Items

telegram_page!(
    "/burger-king",
    pub fn render_burger_king_page() {
        let page = PageLayout::with_header("Burger King Demo", "Burger King Menu");

        let items = [
            MenuItem { id: 1, name: "Whopper", price_cents: 599 },
            MenuItem { id: 2, name: "Cheeseburger", price_cents: 299 },
            MenuItem { id: 3, name: "Chicken Nuggets", price_cents: 399 }
        ];

        for item in &items {
            match render_item(item) {
                Ok(el) => page.append(&el),
                Err(err) => logger::error(&format!("render_item failed: {:?}", err))
            }
        }
    }
);

Step 3: Create Order Button

fn render_item(item: &MenuItem) -> Result<Element, JsValue> {
    let document = document()?;
    let container = document.create_element("div")?;
    container.set_class_name("menu-item");

    // Item label with price
    let label = document.create_element("span")?;
    label.set_inner_html(&format!(
        "{} - ${:.2}",
        item.name,
        item.price_cents as f64 / 100.0
    ));
    container.append_child(&label)?;

    // Order button
    let button_el = telegram_button!(document, "Order", class = "order-button")?;

    // Click handler
    let item_clone = item.clone();
    let click = Closure::<dyn FnMut()>::new(move || {
        if let Some(app) = TelegramWebApp::instance() {
            if let Err(err) = app.send_data(&item_clone.payload()) {
                logger::error(&format!("send_data failed: {:?}", err));
            }
        }
    });

    button_el.set_onclick(Some(click.as_ref().unchecked_ref()));
    click.forget();

    container.append_child(&button_el)?;
    Ok(container)
}

What happens when user clicks "Order":

User clicks "Order"
       ↓
JavaScript onclick fires
       ↓
Rust Closure executes
       ↓
TelegramWebApp::instance()
       ↓
app.send_data(JSON payload)
       ↓
Calls Telegram.WebApp.sendData()
       ↓
Telegram sends data to bot
       ↓
Bot receives web_app_data message
       ↓
Bot handler processes order

telegram_button! macro:

// Expands to:
{
    let btn = document
        .create_element("button")?
        .dyn_into::<HtmlElement>()?;
    btn.set_inner_html("Order");
    btn.set_class_name("order-button");
    Ok(btn)
}

3. Init Data Page (src/pages/init_data.rs)

Purpose: Display user and chat information from Telegram.

How Context Access Works:

use telegram_webapp_sdk::core::safe_context::get_context;

telegram_page!(
    "/init-data",
    pub fn render_init_data_page() {
        let layout = PageLayout::new("Init Data");

        let result = get_context(|ctx| {
            let mut rows = vec![];

            if let Some(user) = &ctx.init_data.user {
                rows.push(DisplayDataRow {
                    title: "id".into(),
                    value: user.id.to_string()
                });
                rows.push(DisplayDataRow {
                    title: "username".into(),
                    value: user.username.clone().unwrap_or_default()
                });
                rows.push(DisplayDataRow {
                    title: "language".into(),
                    value: user.language_code.clone().unwrap_or_default()
                });
            }

            Some(rows)
        });

        // Render rows or error message
        match result {
            Ok(Some(rows)) => {
                match render_display_data("User", &rows) {
                    Ok(section) => layout.append(&section),
                    Err(err) => web_sys::console::error_1(&err.into())
                }
            },
            _ => {
                // Show error fallback
            }
        }
    }
);

get_context implementation:

pub fn get_context<F, R>(f: F) -> Result<R, AppError>
where
    F: FnOnce(&TelegramContext) -> R,
{
    TELEGRAM_CONTEXT.with(|ctx| {
        ctx.borrow()
            .as_ref()
            .ok_or_else(|| AppError::internal("Context not initialized"))
            .map(|context| f(context))
    })
}

Thread-Local Context Storage:

thread_local! {
    static TELEGRAM_CONTEXT: RefCell<Option<TelegramContext>> = RefCell::new(None);
}

Why thread-local?

  • WebAssembly is single-threaded
  • Provides global access without static mut
  • Safe interior mutability with RefCell
  • Automatically initialized by telegram_app!

InitData Structure:

pub struct InitData {
    pub query_id: Option<String>,
    pub user: Option<WebAppUser>,
    pub receiver: Option<WebAppUser>,
    pub chat: Option<WebAppChat>,
    pub chat_type: Option<String>,
    pub chat_instance: Option<String>,
    pub start_param: Option<String>,
    pub can_send_after: Option<u64>,
    pub auth_date: u64,
    pub hash: String,
}

4. Launch Parameters Page (src/pages/launch_params.rs)

Purpose: Display platform and version information.

Key Code:

use telegram_webapp_sdk::core::context::get_launch_params;

telegram_page!(
    "/launch-params",
    pub fn render_launch_params_page() {
        let page = PageLayout::new("Launch Parameters");

        let lp = match get_launch_params() {
            Ok(params) => params,
            Err(err) => {
                web_sys::console::error_1(&err);
                return;
            }
        };

        let rows = vec![
            DisplayDataRow {
                title: "tgWebAppPlatform".into(),
                value: lp.tg_web_app_platform.unwrap_or_else(|| "unknown".into())
            },
            DisplayDataRow {
                title: "tgWebAppVersion".into(),
                value: lp.tg_web_app_version.unwrap_or_else(|| "unknown".into())
            },
            DisplayDataRow {
                title: "tgWebAppStartParam".into(),
                value: lp.tg_web_app_start_param.unwrap_or_else(|| "–".into())
            },
            // ... more fields
        ];

        match render_display_data("Launch Parameters", &rows) {
            Ok(section) => page.append(&section),
            Err(err) => web_sys::console::error_1(&err.into())
        }
    }
);

LaunchParams Structure:

pub struct LaunchParams {
    pub tg_web_app_platform: Option<String>,      // "ios", "android", "web"
    pub tg_web_app_version: Option<String>,       // "7.0"
    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>,      // Inline bot mode
}

Platform Values:

  • "ios" - iPhone, iPad
  • "android" - Android devices
  • "macos" - macOS desktop app
  • "tdesktop" - Windows/Linux desktop app
  • "weba" - Web version (A)
  • "webk" - Web version (K)
  • "unigram" - Unigram client
  • "unknown" - Unknown platform

5. Theme Parameters Page (src/pages/theme_params.rs)

Purpose: Display Telegram app color scheme.

Complete Implementation:

use telegram_webapp_sdk::core::safe_context::get_context;

telegram_page!(
    "/theme-params",
    pub fn render_theme_params_page() {
        let page = PageLayout::new("Theme Parameters");

        let rows: Vec<DisplayDataRow> = get_context(|ctx| {
            ctx.theme_params
                .to_map()
                .into_iter()
                .map(|(key, value)| DisplayDataRow {
                    title: key,
                    value
                })
                .collect()
        })
        .unwrap_or_else(|_| {
            vec![DisplayDataRow {
                title: "Error".into(),
                value: "Failed to load theme params".into()
            }]
        });

        match render_display_data("Theme Params", &rows) {
            Ok(section) => page.append(&section),
            Err(err) => web_sys::console::error_1(&err.into())
        }
    }
);

ThemeParams Structure:

pub struct ThemeParams {
    pub bg_color: Option<String>,
    pub text_color: Option<String>,
    pub hint_color: Option<String>,
    pub link_color: Option<String>,
    pub button_color: Option<String>,
    pub button_text_color: Option<String>,
    pub secondary_bg_color: Option<String>,
    pub header_bg_color: Option<String>,
    pub accent_text_color: Option<String>,
    pub section_bg_color: Option<String>,
    pub section_header_text_color: Option<String>,
    pub subtitle_text_color: Option<String>,
    pub destructive_text_color: Option<String>,
}

Using Theme Colors in 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);
}

How Telegram injects CSS variables:

Telegram automatically injects CSS variables based on theme:

  • --tg-theme-bg-color
  • --tg-theme-text-color
  • --tg-theme-hint-color
  • etc.

These are available immediately, no SDK initialization required.


Components Explained

1. PageLayout Component (src/components/page_layout.rs)

Purpose: Consistent page wrapper with title and content area.

Implementation:

use web_sys::{Document, Element, window};

pub struct PageLayout {
    pub root: Element
}

impl PageLayout {
    pub fn new(title: &str) -> Self {
        let document = document();

        // Set browser tab title
        document.set_title(title);

        // Get #app-root container
        let root = document
            .get_element_by_id("app-root")
            .expect("Expected <div id='app-root'> to exist");

        // Clear previous content
        root.set_inner_html("");

        Self { root }
    }

    pub fn with_header(title: &str, header: &str) -> Self {
        let layout = Self::new(title);

        // Create <h1> header
        let h1 = document()
            .create_element("h1")
            .expect("failed to create <h1>");
        h1.set_inner_html(header);

        layout.root.append_child(&h1).unwrap();
        layout
    }

    pub fn append(&self, element: &Element) {
        let _ = self.root.append_child(element);
    }
}

fn document() -> Document {
    window().unwrap().document().unwrap()
}

Usage Example:

let page = PageLayout::new("My Page");
page.append(&some_element);
page.append(&another_element);

2. DisplayData Component (src/components/display_data.rs)

Purpose: Render key-value data rows with color swatches.

Key Features:

  • Displays data as title-value pairs
  • Detects #RRGGBB colors and shows swatches
  • Error handling with masterror

Implementation:

use masterror::{AppError, AppResult};
use web_sys::{Element, window};
use crate::components::rgb::RGB;

pub struct DisplayDataRow {
    pub title: String,
    pub value: String
}

pub fn render_display_data(header: &str, rows: &[DisplayDataRow]) -> AppResult<Element> {
    let document = window()
        .ok_or_else(|| AppError::internal("no window"))?
        .document()
        .ok_or_else(|| AppError::internal("no document"))?;

    // Create <section class="display-data">
    let section = document
        .create_element("section")
        .map_err(|_| AppError::internal("create section"))?;
    section.set_class_name("display-data");

    // Add header <h3>
    let h3 = document.create_element("h3")
        .map_err(|_| AppError::internal("create h3"))?;
    h3.set_class_name("display-data-header");
    h3.set_inner_html(header);
    section.append_child(&h3)
        .map_err(|_| AppError::internal("append h3"))?;

    // Add rows
    for row in rows {
        let row_el = document.create_element("div")
            .map_err(|_| AppError::internal("create row"))?;
        row_el.set_class_name("display-data-row");

        // Title span
        let title_el = document.create_element("span")
            .map_err(|_| AppError::internal("create title"))?;
        title_el.set_class_name("display-data-title");
        title_el.set_inner_html(&row.title);

        // Value span
        let value_el = document.create_element("span")
            .map_err(|_| AppError::internal("create value"))?;
        value_el.set_class_name("display-data-value");

        // If value is color, render swatch
        if row.value.starts_with('#') {
            let swatch = RGB::render(&row.value)?;
            value_el.append_child(&swatch)
                .map_err(|_| AppError::internal("append swatch"))?;
        } else {
            value_el.set_text_content(Some(&row.value));
        }

        row_el.append_child(&title_el)
            .map_err(|_| AppError::internal("append title"))?;
        row_el.append_child(&value_el)
            .map_err(|_| AppError::internal("append value"))?;
        section.append_child(&row_el)
            .map_err(|_| AppError::internal("append row"))?;
    }

    Ok(section)
}

HTML Output:

<section class="display-data">
    <h3 class="display-data-header">User</h3>
    <div class="display-data-row">
        <span class="display-data-title">id</span>
        <span class="display-data-value">12345</span>
    </div>
    <div class="display-data-row">
        <span class="display-data-title">bg_color</span>
        <span class="display-data-value">
            <span class="rgb">
                <i class="rgb__icon" style="background-color: #ffffff"></i>
                #ffffff
            </span>
        </span>
    </div>
</section>

3. RGB Component (src/components/rgb.rs)

Purpose: Render color swatch with hex value.

Implementation:

use masterror::{AppError, AppResult};
use wasm_bindgen::JsCast;
use web_sys::{HtmlElement, window};

pub struct RGB;

impl RGB {
    pub fn render(color: &str) -> AppResult<HtmlElement> {
        let doc = window()
            .ok_or_else(|| AppError::internal("no window"))?
            .document()
            .ok_or_else(|| AppError::internal("no document"))?;

        // <span class="rgb">
        let span = doc
            .create_element("span")
            .map_err(|_| AppError::internal("create span"))?
            .dyn_into::<HtmlElement>()
            .map_err(|_| AppError::internal("span into HtmlElement"))?;
        span.set_class_name("rgb");

        // <i class="rgb__icon" style="background-color: #RRGGBB"></i>
        let icon = doc
            .create_element("i")
            .map_err(|_| AppError::internal("create icon"))?
            .dyn_into::<HtmlElement>()
            .map_err(|_| AppError::internal("icon into HtmlElement"))?;
        icon.set_class_name("rgb__icon");
        icon.style()
            .set_property("background-color", color)
            .map_err(|_| AppError::internal("set color"))?;
        span.append_child(&icon)
            .map_err(|_| AppError::internal("append icon"))?;

        // Text node: "#RRGGBB"
        let text = doc.create_text_node(color);
        span.append_child(&text)
            .map_err(|_| AppError::internal("append text"))?;

        Ok(span)
    }
}

CSS Styling:

.rgb {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

.rgb__icon {
    display: inline-block;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    border: 1px solid rgba(0, 0, 0, 0.1);
}

Visual Result:

[🔵] #5288C1

(Circle with background color + hex value)


4. NavLink Component (src/components/nav_link.rs)

Purpose: Clickable navigation link with title and subtitle.

Implementation:

use wasm_bindgen::{JsCast, prelude::Closure};
use web_sys::{Document, Element, HtmlElement, window};

pub fn nav_link(label: &str, subtitle: Option<&str>, href: &str) -> Element {
    let document = document();
    let link = document.create_element("div").unwrap();
    link.set_class_name("nav-link");

    // Title
    let title = document.create_element("div").unwrap();
    title.set_class_name("label");
    title.set_inner_html(label);
    link.append_child(&title).unwrap();

    // Subtitle (optional)
    if let Some(sub) = subtitle {
        let subtitle_el = document.create_element("div").unwrap();
        subtitle_el.set_class_name("subtitle");
        subtitle_el.set_inner_html(sub);
        link.append_child(&subtitle_el).unwrap();
    }

    // Click handler
    let link_closure = {
        let href = href.to_string();
        Closure::<dyn FnMut()>::new(move || {
            if let Some(window) = window() {
                let _ = window.location().set_href(&href);
            }
        })
    };

    let html_elem: HtmlElement = link.clone().dyn_into().unwrap();
    html_elem.set_onclick(Some(link_closure.as_ref().unchecked_ref()));
    link_closure.forget(); // Intentional leak

    link
}

fn document() -> Document {
    window().unwrap().document().unwrap()
}

Why window.location.set_href?

  • Changes browser URL
  • Triggers router's popstate listener
  • Navigates to new route without page reload

5. DevMenu Component (src/components/dev_menu.rs)

Purpose: Development controls for testing SDK features.

Button Definitions:

type Handler = fn(&TelegramWebApp) -> Result<(), JsValue>;

const BUTTON_IDS: &[(&str, Handler)] = &[
    ("send-data", |tg| tg.send_data("Hello from Dev Menu!")),
    ("expand", |tg| tg.expand()),
    ("close", |tg| tg.close()),
    ("alert", |tg| tg.show_alert("This is a test alert")),
    ("main-button", |tg| {
        tg.set_bottom_button_text(BottomButton::Main, "Clicked!")?;
        tg.show_bottom_button(BottomButton::Main)?;
        Ok(())
    }),
    ("is-expanded", |tg| {
        let expanded = tg.is_expanded();
        if expanded {
            tg.show_alert("Already expanded")?;
        } else {
            tg.expand()?;
        }
        Ok(())
    }),
    ("add-to-home-screen", |tg| {
        if let Ok(shown) = tg.add_to_home_screen() {
            info(&format!("shown = {}", shown));
        }
        Ok(())
    }),
    ("check-home-screen", |tg| {
        tg.check_home_screen_status(|status| {
            info(&format!("status: {}", status));
        })?;
        Ok(())
    })
];

Setup Function:

pub fn setup_dev_menu() {
    let doc = match window().and_then(|w| w.document()) {
        Some(doc) => doc,
        None => return  // Not in browser
    };

    for (id, handler) in BUTTON_IDS {
        if let Some(elem) = doc
            .get_element_by_id(id)
            .and_then(|e| e.dyn_into::<HtmlElement>().ok())
        {
            let handler = *handler;

            let cb = Closure::<dyn FnMut()>::new(move || {
                if let Some(tg) = TelegramWebApp::instance()
                    && let Err(err) = handler(&tg)
                {
                    web_sys::console::error_1(&err);
                }
            });

            elem.set_onclick(Some(cb.as_ref().unchecked_ref()));
            cb.forget();
        }
    }
}

HTML in index.html:

<footer id="dev-menu">
    <button id="send-data">Send Data</button>
    <button id="expand">Expand</button>
    <button id="close">Close</button>
    <button id="alert">Show Alert</button>
    <button id="main-button">Main Button</button>
    <button id="is-expanded">Is Expanded?</button>
    <button id="add-to-home-screen">Add to Home</button>
    <button id="check-home-screen">Check Home</button>
</footer>

Bot Backend Integration

Bot Structure (examples/bots/rust_bot/)

Purpose: Receive data from WebApp and respond to users.

Project Files:

examples/bots/rust_bot/
├── src/
│   ├── main.rs          # Bot logic with handlers
│   └── lib.rs           # OrderData structure
├── Cargo.toml           # Dependencies
├── .env.example         # Environment template
└── README.md            # Documentation

Dependencies (Cargo.toml)

[package]
name = "webapp-bot-example"
version = "0.1.0"
edition = "2024"
rust-version = "1.90"

[dependencies]
teloxide = { version = "0.17", features = ["macros"] }  # Telegram Bot API
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }  # Async runtime
dotenvy = "0.15"                                        # Load .env files
serde = { version = "1", features = ["derive"] }        # Serialization
serde_json = "1"                                        # JSON parsing
tracing = "0.1"                                         # Logging
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
masterror = "0.24"                                      # Error handling

Environment Configuration (.env)

TELOXIDE_TOKEN=110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
WEBAPP_URL=https://your-domain.com/demo/index.html
RUST_LOG=info

Security Note: Never commit .env file to Git!

OrderData Structure (src/lib.rs)

Purpose: Define data structure matching WebApp payload.

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct OrderData {
    pub id:          u32,
    pub name:        String,
    pub price_cents: u32
}

Must match WebApp JSON:

{"id": 1, "name": "Whopper", "price_cents": 599}

Comprehensive Tests:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_order_data_deserialize() {
        let json = r#"{"id": 1, "name": "Whopper", "price_cents": 599}"#;
        let order: OrderData = serde_json::from_str(json).expect("valid json");

        assert_eq!(order.id, 1);
        assert_eq!(order.name, "Whopper");
        assert_eq!(order.price_cents, 599);
    }

    #[test]
    fn test_order_data_serialize() {
        let order = OrderData {
            id: 2,
            name: "Big King".to_string(),
            price_cents: 499
        };

        let json = serde_json::to_string(&order).expect("serialize");
        assert!(json.contains("\"id\":2"));
    }

    #[test]
    fn test_order_data_missing_field() {
        let json = r#"{"id": 1, "name": "Whopper"}"#;
        let result: Result<OrderData, _> = serde_json::from_str(json);
        assert!(result.is_err());
    }

    #[test]
    fn test_order_price_calculation() {
        let order = OrderData {
            id: 1,
            name: "Test".to_string(),
            price_cents: 1234
        };
        let price_dollars = order.price_cents as f64 / 100.0;
        assert_eq!(price_dollars, 12.34);
    }

    #[test]
    fn test_order_data_roundtrip() {
        let original = OrderData {
            id: 42,
            name: "Chicken Royale".to_string(),
            price_cents: 750
        };

        let json = serde_json::to_string(&original).expect("serialize");
        let deserialized: OrderData = serde_json::from_str(&json).expect("deserialize");

        assert_eq!(original, deserialized);
    }
}

Bot Main Logic (src/main.rs)

Initialization:

#[tokio::main]
async fn main() {
    // Initialize logging
    tracing_subscriber::fmt::init();

    // Load .env file
    dotenvy::dotenv().ok();

    // Get WebApp URL from environment
    let webapp_url = std::env::var("WEBAPP_URL")
        .unwrap_or_else(|_| "https://example.com/index.html".to_string());

    tracing::info!("Starting WebApp bot with URL: {}", webapp_url);

    // Create bot from TELOXIDE_TOKEN env var
    let bot = Bot::from_env();

    // Build handler tree
    let handler = Update::filter_message()
        .branch(
            dptree::entry()
                .filter_command::<Command>()
                .endpoint(handle_command)
        )
        .branch(
            dptree::filter(|msg: Message| msg.web_app_data().is_some())
                .endpoint(handle_webapp_data)
        );

    // Start dispatcher
    Dispatcher::builder(bot, handler)
        .enable_ctrlc_handler()
        .build()
        .dispatch()
        .await;
}

Command Definitions:

#[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase")]
enum Command {
    #[command(description = "Display welcome message")]
    Start,
    #[command(description = "Show help information")]
    Help
}

Command Handler:

async fn handle_command(bot: Bot, msg: Message, cmd: Command) -> Result<(), AppError> {
    let webapp_url = std::env::var("WEBAPP_URL")
        .unwrap_or_else(|_| "https://example.com/index.html".to_string());

    match cmd {
        Command::Start => {
            // Create inline keyboard with WebApp buttons
            let keyboard = InlineKeyboardMarkup::new(vec![
                vec![InlineKeyboardButton::web_app(
                    "Open Burger King Menu",
                    WebAppInfo {
                        url: format!("{}#/burger-king", webapp_url)
                            .parse()
                            .map_err(|e| {
                                AppError::new(AppErrorKind::Internal, "Invalid URL")
                                    .with_context(e)
                            })?
                    }
                )],
                vec![InlineKeyboardButton::web_app(
                    "View Init Data",
                    WebAppInfo {
                        url: format!("{}#/init-data", webapp_url).parse()
                            .map_err(|e| {
                                AppError::new(AppErrorKind::Internal, "Invalid URL")
                                    .with_context(e)
                            })?
                    }
                )],
                vec![InlineKeyboardButton::web_app(
                    "Theme Parameters",
                    WebAppInfo {
                        url: format!("{}#/theme-params", webapp_url).parse()
                            .map_err(|e| {
                                AppError::new(AppErrorKind::Internal, "Invalid URL")
                                    .with_context(e)
                            })?
                    }
                )],
            ]);

            bot.send_message(
                msg.chat.id,
                "Welcome to Telegram WebApp SDK Demo!\n\nClick a button to open the WebApp:"
            )
            .reply_markup(keyboard)
            .await
            .map_err(|e| {
                AppError::new(AppErrorKind::Service, "Failed to send message")
                    .with_context(e)
            })?;
        }
        Command::Help => {
            bot.send_message(
                msg.chat.id,
                "This bot demonstrates telegram-webapp-sdk.\n\n\
                 Commands:\n\
                 /start - Open WebApp menu\n\
                 /help - Show this message\n\n\
                 GitHub: https://github.com/RAprogramm/telegram-webapp-sdk"
            )
            .await
            .map_err(|e| {
                AppError::new(AppErrorKind::Service, "Failed to send message")
                    .with_context(e)
            })?;
        }
    }

    Ok(())
}

WebApp Data Handler:

async fn handle_webapp_data(bot: Bot, msg: Message) -> Result<(), AppError> {
    if let Some(web_app_data) = msg.web_app_data() {
        // Parse JSON from WebApp
        let order: OrderData = serde_json::from_str(&web_app_data.data)
            .map_err(|e| {
                AppError::new(AppErrorKind::BadRequest, "Invalid order data format")
                    .with_context(e)
            })?;

        let price_dollars = order.price_cents as f64 / 100.0;

        // Format response message
        let response = format!(
            "Order Received!\n\n\
             Item: {}\n\
             Price: ${:.2}\n\
             Order ID: #{}\n\n\
             Your order is being processed...",
            order.name, price_dollars, order.id
        );

        // Send confirmation
        bot.send_message(msg.chat.id, response)
            .await
            .map_err(|e| {
                AppError::new(AppErrorKind::Service, "Failed to send message")
                    .with_context(e)
            })?;

        // Log to server
        tracing::info!(
            "Order from user {}: {} (${:.2})",
            msg.from.as_ref().map(|u| u.id.0).unwrap_or(0),
            order.name,
            price_dollars
        );
    }

    Ok(())
}

Complete Data Flow

End-to-End Order Flow

┌─────────────────────────────────────────────────────────────┐
│                    1. User Opens Bot                         │
│  User sends /start → Bot receives command                    │
└────────────────────────────┬────────────────────────────────┘
                             ↓
┌─────────────────────────────────────────────────────────────┐
│               2. Bot Sends WebApp Button                     │
│  handle_command() → InlineKeyboardButton::web_app()          │
│  URL: https://domain.com/demo/index.html#/burger-king        │
└────────────────────────────┬────────────────────────────────┘
                             ↓
┌─────────────────────────────────────────────────────────────┐
│               3. User Clicks Button                          │
│  Telegram opens WebApp in in-app browser                     │
│  Loads WASM bundle                                           │
└────────────────────────────┬────────────────────────────────┘
                             ↓
┌─────────────────────────────────────────────────────────────┐
│              4. WebApp Initializes                           │
│  telegram_app! macro expands                                 │
│  Parses tgWebAppData from URL                                │
│  Stores context in thread-local                              │
│  Calls main()                                                │
└────────────────────────────┬────────────────────────────────┘
                             ↓
┌─────────────────────────────────────────────────────────────┐
│              5. Router Starts                                │
│  telegram_router!() collects all pages                       │
│  Builds routing table                                        │
│  Reads current path: /burger-king                            │
│  Calls render_burger_king_page()                             │
└────────────────────────────┬────────────────────────────────┘
                             ↓
┌─────────────────────────────────────────────────────────────┐
│              6. Page Renders                                 │
│  Creates PageLayout                                          │
│  Renders 3 menu items                                        │
│  Each item has "Order" button                                │
│  User sees menu                                              │
└────────────────────────────┬────────────────────────────────┘
                             ↓
┌─────────────────────────────────────────────────────────────┐
│           7. User Selects Item                               │
│  User clicks "Order" on "Whopper"                            │
│  JavaScript onclick fires                                    │
│  Rust Closure executes                                       │
└────────────────────────────┬────────────────────────────────┘
                             ↓
┌─────────────────────────────────────────────────────────────┐
│           8. WebApp Sends Data                               │
│  TelegramWebApp::instance()                                  │
│  app.send_data('{"id":1,"name":"Whopper","price_cents":599}')│
│  Calls Telegram.WebApp.sendData()                            │
└────────────────────────────┬────────────────────────────────┘
                             ↓
┌─────────────────────────────────────────────────────────────┐
│        9. Telegram Delivers to Bot                           │
│  Telegram sends web_app_data message to bot                  │
│  Message includes JSON payload                               │
└────────────────────────────┬────────────────────────────────┘
                             ↓
┌─────────────────────────────────────────────────────────────┐
│         10. Bot Processes Order                              │
│  handle_webapp_data() receives message                       │
│  Parses JSON: OrderData { id: 1, name: "Whopper", ... }     │
│  Formats confirmation message                                │
│  Sends message to user                                       │
│  Logs order to server                                        │
└────────────────────────────┬────────────────────────────────┘
                             ↓
┌─────────────────────────────────────────────────────────────┐
│        11. User Receives Confirmation                        │
│  "Order Received! Item: Whopper - $5.99"                     │
│  User sees confirmation in chat                              │
└─────────────────────────────────────────────────────────────┘

Message Structure

WebApp Data Message:

{
  "update_id": 123456789,
  "message": {
    "message_id": 12345,
    "from": {
      "id": 123456789,
      "is_bot": false,
      "first_name": "John",
      "username": "johndoe"
    },
    "chat": {
      "id": 123456789,
      "first_name": "John",
      "username": "johndoe",
      "type": "private"
    },
    "date": 1699564800,
    "web_app_data": {
      "button_text": "Open Burger King Menu",
      "data": "{\"id\":1,\"name\":\"Whopper\",\"price_cents\":599}"
    }
  }
}

Key Field: web_app_data.data

  • Contains JSON string sent by sendData()
  • Bot parses this string to get order details

Running and Testing

Complete step-by-step guide to running the demo locally with maximum detail.

System Requirements

Operating Systems:

  • Linux (Ubuntu 20.04+, Debian 11+, Arch, Fedora)
  • macOS (10.15+)
  • Windows 10/11 with WSL2 (recommended) or native

Minimum Hardware:

  • CPU: 2 cores
  • RAM: 4 GB
  • Disk: 2 GB free space
  • Internet connection

Part 1: Installing Prerequisites

1.1 Install Rust Toolchain

What is Rust? Rust is the programming language used to build the demo. You need Rust 1.90.0 or later (MSRV for SDK 0.3.0).

Installation Steps:

Linux/macOS:

# Download and run rustup installer
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Interactive prompts:

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
> 1

After installation completes:

# Add Rust to your PATH (current session)
source "$HOME/.cargo/env"

# Verify installation
rustc --version

Expected output:

rustc 1.90.0 (cccfff8cf 2025-10-12)

If version is less than 1.90.0, update:

rustup update stable

Windows (native):

Download installer from: https://rustup.rs/

Run rustup-init.exe and follow prompts.

Windows (WSL2 - Recommended):

# Inside WSL2 Ubuntu terminal
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"

Reference: Official Rust Installation Guide


1.2 Add WebAssembly Target

What is wasm32-unknown-unknown? This is the compilation target for browser-based WebAssembly. Required for all Rust WASM applications.

Installation:

rustup target add wasm32-unknown-unknown

Expected output:

info: downloading component 'rust-std' for 'wasm32-unknown-unknown'
info: installing component 'rust-std' for 'wasm32-unknown-unknown'

Verify installation:

rustup target list --installed | grep wasm32

Expected output:

wasm32-unknown-unknown

1.3 Install Trunk Build Tool

What is Trunk? Trunk is the official recommended bundler for Yew and Rust WASM applications. It:

  • Compiles Rust to WASM
  • Bundles JavaScript and CSS
  • Provides hot-reload development server
  • Optimizes for production

Installation:

cargo install --locked trunk

Why --locked? Uses exact dependency versions from Cargo.lock for reproducible builds.

Installation time: 5-10 minutes (compiles from source)

Output during installation:

    Updating crates.io index
  Downloaded trunk v0.21.9
  Downloaded 1 crate (120.5 KB) in 0.52s
   Compiling proc-macro2 v1.0.94
   Compiling unicode-ident v1.0.14
   ...
   Compiling trunk v0.21.9
    Finished release [optimized] target(s) in 8m 32s
  Installing ~/.cargo/bin/trunk
   Installed package `trunk v0.21.9` (executable `trunk`)

Verify installation:

trunk --version

Expected output:

trunk 0.21.9

Reference: Trunk Official Website


1.4 Install Tunneling Tool (for HTTPS)

Telegram requires HTTPS for WebApps. For local development, use a tunneling tool.

Option A: cloudflared (Recommended)

What is cloudflared? Free tunneling tool by Cloudflare. Creates public HTTPS URL pointing to your localhost.

Advantages:

  • No account required for basic use
  • Fast and reliable
  • Free forever
  • No request limits

Installation:

macOS:

brew install cloudflare/cloudflare/cloudflared

Linux (Debian/Ubuntu):

# Download latest release
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb

# Install
sudo dpkg -i cloudflared-linux-amd64.deb

# Verify
cloudflared --version

Linux (Arch):

sudo pacman -S cloudflared

Linux (other distributions):

# Download binary
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64

# Make executable
chmod +x cloudflared-linux-amd64

# Move to PATH
sudo mv cloudflared-linux-amd64 /usr/local/bin/cloudflared

# Verify
cloudflared --version

Windows: Download from: https://github.com/cloudflare/cloudflared/releases

Extract and add to PATH.

Reference: cloudflared Installation Guide


Option B: ngrok

What is ngrok? Popular tunneling service with free and paid tiers.

Advantages:

  • Web interface to inspect requests
  • Custom domains (paid)
  • Request replay (paid)

Disadvantages:

  • Requires account (free available)
  • URL changes on each restart (free tier)

Installation:

macOS:

brew install ngrok/ngrok/ngrok

Linux:

# Download and install
curl -sSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
  | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null \
  && echo "deb https://ngrok-agent.s3.amazonaws.com buster main" \
  | sudo tee /etc/apt/sources.list.d/ngrok.list \
  && sudo apt update \
  && sudo apt install ngrok

Or download binary:

# Download
wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz

# Extract
tar xvzf ngrok-v3-stable-linux-amd64.tgz

# Move to PATH
sudo mv ngrok /usr/local/bin/

# Verify
ngrok version

Windows: Download from: https://ngrok.com/download

Setup authtoken (required):

# Sign up at https://dashboard.ngrok.com/signup
# Get token from https://dashboard.ngrok.com/get-started/your-authtoken

ngrok config add-authtoken YOUR_AUTHTOKEN

Reference: ngrok Installation Guide


1.5 Create Telegram Bot

What is @BotFather? Official Telegram bot for creating and managing bots.

Step-by-Step:

  1. Open Telegram on any device

  2. Search for @BotFather or open: https://t.me/BotFather

  3. Send /newbot command

    You: /newbot
    
    BotFather: Alright, a new bot. How are we going to call it?
               Please choose a name for your bot.
    
  4. Enter bot name (display name, can be anything)

    You: My WebApp Demo Bot
    
    BotFather: Good. Now let's choose a username for your bot.
               It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.
    
  5. Enter bot username (must be unique and end with "bot")

    You: my_webapp_demo_bot
    
    BotFather: Done! Congratulations on your new bot.
               You will find it at t.me/my_webapp_demo_bot.
    
               Use this token to access the HTTP API:
               110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
               Keep your token secure and store it safely...
    
  6. Save your bot token - Copy and store securely

    CRITICAL: Never commit this token to Git or share publicly!

Reference: Telegram Bot API Documentation


Part 2: Running the Demo WebApp

2.1 Clone Repository

What are we cloning? The telegram-webapp-sdk repository containing the demo and SDK source code.

Clone with Git:

# Navigate to your projects directory
cd ~/Projects  # or wherever you keep projects

# Clone repository
git clone https://github.com/RAprogramm/telegram-webapp-sdk.git

# Navigate to demo directory
cd telegram-webapp-sdk/demo

Expected output:

Cloning into 'telegram-webapp-sdk'...
remote: Enumerating objects: 1234, done.
remote: Counting objects: 100% (1234/1234), done.
remote: Compressing objects: 100% (567/567), done.
remote: Total 1234 (delta 789), reused 1100 (delta 670)
Receiving objects: 100% (1234/1234), 3.45 MiB | 2.30 MiB/s, done.
Resolving deltas: 100% (789/789), done.

Verify directory structure:

ls -la

Expected output:

total 28
drwxr-xr-x  4 user user 4096 Nov  4 10:00 .
drwxr-xr-x 16 user user 4096 Nov  4 10:00 ..
-rw-r--r--  1 user user  560 Nov  4 10:00 Cargo.toml
drwxr-xr-x  2 user user 4096 Nov  4 10:00 dist
-rw-r--r--  1 user user 1056 Nov  4 10:00 index.html
-rw-r--r--  1 user user 6436 Nov  4 10:00 README.md
drwxr-xr-x  4 user user 4096 Nov  4 10:00 src
-rw-r--r--  1 user user 2561 Nov  4 10:00 style.css
-rw-r--r--  1 user user   30 Nov  4 10:00 Trunk.toml

2.2 Build and Run Development Server

What does trunk serve do?

  • Compiles Rust to WASM
  • Starts development server on http://localhost:8080
  • Watches for file changes
  • Auto-reloads browser on changes

Start development server:

trunk serve

First run takes longer (5-15 minutes):

  • Downloads all dependencies
  • Compiles entire project
  • Subsequent runs are much faster (<30 seconds)

Expected output (detailed):

2025-11-04T10:00:00.000Z INFO 📦 starting build

Trunk starts building process

2025-11-04T10:00:01.000Z INFO spawning asset pipelines

Processing index.html, style.css

2025-11-04T10:00:02.000Z INFO building telegram-webapp-sdk-demo

Compiling Rust to WASM

    Updating crates.io index

Downloading dependency metadata

 Downloading crates ...
  Downloaded wasm-bindgen v0.2.104
  Downloaded web-sys v0.3.81
  Downloaded telegram-webapp-sdk v0.3.0
  ...

Downloading ~50-100 crates

   Compiling proc-macro2 v1.0.94
   Compiling unicode-ident v1.0.14
   Compiling syn v2.0.123
   ...

Compiling dependencies (takes longest)

   Compiling demo v0.2.6 (/path/to/demo)

Compiling your demo code

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 8m 32s

Compilation complete

2025-11-04T10:08:30.000Z INFO ✅ success

Build successful

2025-11-04T10:08:30.000Z INFO 📡 serving on http://127.0.0.1:8080

Server running - demo is ready!

What if compilation fails?

Common error: Missing wasm-bindgen-cli

error: failed to run `wasm-bindgen`

Solution:

cargo install wasm-bindgen-cli --version 0.2.104

Match version to what Cargo.lock specifies.

Server is now running!

Open browser to http://localhost:8080 to see demo.

To stop server: Press Ctrl+C


2.3 Create HTTPS Tunnel

Why HTTPS? Telegram Web Apps MUST be served over HTTPS for security. Localhost HTTP won't work in Telegram.

Open second terminal window/tab (keep trunk serve running in first)

Option A: Using cloudflared

cloudflared tunnel --url http://localhost:8080

Expected output:

2025-11-04T10:10:00Z INF Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
2025-11-04T10:10:01Z INF Requesting new quick Tunnel on trycloudflare.com...
2025-11-04T10:10:02Z INF +--------------------------------------------------------------------------------------------+
2025-11-04T10:10:02Z INF |  Your quick Tunnel has been created! Visit it at (it may take some time to be reachable): |
2025-11-04T10:10:02Z INF |  https://random-words-1234.trycloudflare.com                                              |
2025-11-04T10:10:02Z INF +--------------------------------------------------------------------------------------------+
2025-11-04T10:10:03Z INF Connection registered connIndex=0 connection=abc123... location=SJC

Your tunnel URL: https://random-words-1234.trycloudflare.com

Copy this URL - you'll need it for BotFather configuration.

Test tunnel is working:

Open in browser: https://random-words-1234.trycloudflare.com/index.html

You should see the demo app.

Keep this terminal open! Tunnel stays active while running.


Option B: Using ngrok

ngrok http 8080

Expected output:

ngrok                                                                     (Ctrl+C to quit)

Build better APIs with ngrok. Early access: ngrok.com/early-access

Session Status                online
Account                       YourName (Plan: Free)
Version                       3.18.4
Region                        United States (us)
Latency                       -
Web Interface                 http://127.0.0.1:4040
Forwarding                    https://abc123.ngrok-free.app -> http://localhost:8080

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

Your tunnel URL: https://abc123.ngrok-free.app

Web Interface: Open http://127.0.0.1:4040 to see requests in real-time

Test tunnel:

Open in browser: https://abc123.ngrok-free.app/index.html

Keep this terminal open!


Important Notes:

  1. URL changes on restart - When you stop cloudflared/ngrok and restart, you get a new random URL

  2. Update BotFather every time - Each new URL must be configured in BotFather

  3. For persistent URLs:

    • cloudflared: Create named tunnel (requires account)
    • ngrok: Upgrade to paid plan for custom domains
  4. Security: These URLs are public - anyone with URL can access your local server


2.4 Configure Bot in BotFather

Now we connect the WebApp URL to your bot.

Step-by-Step:

  1. Open @BotFather in Telegram

  2. Send command: /mybots

    BotFather: Choose a bot from the list below:
    
               [My WebApp Demo Bot]
    
  3. Click your bot name or send its name

  4. BotFather shows menu:

    [Edit Bot]
    [Bot Settings]
    [Payments]
    [Transfer Ownership]
    [Delete Bot]
    
  5. Click or send: Bot Settings

  6. Settings menu appears:

    [Edit Name]
    [Edit Description]
    [Edit About]
    [Edit Botpic]
    [Edit Commands]
    [Delete Bot]
    [Menu Button]
    
  7. Click or send: Menu Button

  8. Menu Button options:

    Current menu button: default
    
    [Edit menu button URL]
    [Edit menu button text]
    [Reset menu button]
    
  9. Click or send: Edit menu button URL

  10. BotFather asks for URL:

    BotFather: Send me the URL of the Web App.
    
  11. Send your tunnel URL:

    You: https://random-words-1234.trycloudflare.com/index.html
    

    CRITICAL: Include /index.html at the end!

  12. BotFather confirms:

    BotFather: Success! The menu button URL was updated.
    

Configuration complete!


2.5 Test WebApp in Telegram

Final step: Test everything works!

  1. Open your bot in Telegram

    • Search for your bot username
    • Or open t.me/your_bot_username
  2. Look at the bottom of the chat

    • You should see a button with hamburger icon (≡)
    • This is the menu button
  3. Click the menu button

    • WebApp opens in Telegram's in-app browser
    • You should see: "Telegram WebApp SDK Demo"
  4. Explore the demo:

    • Click "Burger King Demo" - see menu items
    • Click "Init Data" - see your user information
    • Click "Launch Parameters" - see platform info
    • Click "Theme Parameters" - see color scheme

If WebApp doesn't load:

  • Check tunnel is still running
  • Verify URL in BotFather includes /index.html
  • Check trunk serve is still running
  • Try opening tunnel URL directly in browser first

Success! WebApp is working in Telegram!

But buttons don't send data yet - we need the bot backend...


Part 3: Running the Bot Backend

Now we set up the backend to receive data from WebApp.

3.1 Navigate to Bot Directory

# Open new terminal (keep trunk serve and tunnel running)
cd ~/Projects/telegram-webapp-sdk/examples/bots/rust_bot

Verify directory contents:

ls -la

Expected output:

total 20
drwxr-xr-x 3 user user 4096 Nov  4 10:00 .
drwxr-xr-x 3 user user 4096 Nov  4 10:00 ..
-rw-r--r-- 1 user user  123 Nov  4 10:00 .env.example
-rw-r--r-- 1 user user  560 Nov  4 10:00 Cargo.toml
-rw-r--r-- 1 user user 3456 Nov  4 10:00 README.md
drwxr-xr-x 2 user user 4096 Nov  4 10:00 src

3.2 Create Environment File

What is .env file? Configuration file storing sensitive information like bot tokens. Never commit to Git!

Create from template:

cp .env.example .env

Edit with your favorite editor:

# Choose one:
nano .env        # Simple terminal editor
vim .env         # Vim
code .env        # VS Code
gedit .env       # GNOME Text Editor
kate .env        # KDE Kate

3.3 Configure Environment Variables

Edit .env file content:

# Telegram Bot Token from @BotFather
# Replace with YOUR actual token!
TELOXIDE_TOKEN=110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw

# WebApp URL from cloudflared/ngrok tunnel
# Replace with YOUR actual tunnel URL!
# IMPORTANT: Include /index.html at the end!
WEBAPP_URL=https://random-words-1234.trycloudflare.com/index.html

# Logging level: trace, debug, info, warn, error
RUST_LOG=info

Replace these values:

  1. TELOXIDE_TOKEN

    • Get from @BotFather (when you created bot)
    • Format: NUMBER:ALPHANUMERIC_STRING
    • Example: 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
  2. WEBAPP_URL

    • Your cloudflared/ngrok tunnel URL
    • Must include /index.html
    • Example: https://random-words-1234.trycloudflare.com/index.html
  3. RUST_LOG (optional)

    • info - Normal logging (recommended)
    • debug - Detailed logging
    • trace - Very detailed logging
    • warn - Only warnings and errors
    • error - Only errors

Save file and exit editor.

Verify .env file:

cat .env

Should show your configuration.

Security check:

# Ensure .env is in .gitignore
grep "\.env" .gitignore

Should output .env - this prevents accidental commits.


3.4 Build and Run Bot

What happens during first run?

  • Downloads all dependencies (~100 crates)
  • Compiles bot and dependencies
  • Connects to Telegram servers
  • Starts listening for messages

Run bot:

cargo run --release

Why --release?

  • Optimized build (faster runtime)
  • Smaller binary size
  • Longer compile time (worth it)

Expected output (detailed):

   Compiling libc v0.2.168
   Compiling cfg-if v1.0.0
   Compiling proc-macro2 v1.0.94
   ...

Compiling dependencies (takes 5-10 minutes first time)

   Compiling webapp-bot-example v0.1.0 (/path/to/rust_bot)

Compiling bot code

    Finished `release` profile [optimized] target(s) in 7m 45s
     Running `target/release/webapp-bot-example`

Compilation complete, bot starting

2025-11-04T10:15:00.000Z  INFO webapp_bot_example: Starting WebApp bot with URL: https://random-words-1234.trycloudflare.com/index.html

Bot initialized with your WebApp URL

2025-11-04T10:15:01.000Z  INFO teloxide::dispatching: Listening for updates

Bot is running! Listening for Telegram messages.

Bot is now online and ready!


3.5 Test Complete Integration

Now test the full flow: WebApp → Bot

  1. Open your bot in Telegram

  2. Send command: /start

  3. Bot responds with buttons:

    Welcome to Telegram WebApp SDK Demo!
    
    Click a button to open the WebApp:
    
    [Open Burger King Menu]
    [View Init Data]
    [Theme Parameters]
    
  4. Click: Open Burger King Menu

  5. WebApp opens showing menu:

    Burger King Menu
    
    Whopper - $5.99    [Order]
    Cheeseburger - $2.99    [Order]
    Chicken Nuggets - $3.99    [Order]
    
  6. Click "Order" on any item (e.g., Whopper)

  7. Bot immediately responds in chat:

    Order Received!
    
    Item: Whopper
    Price: $5.99
    Order ID: #1
    
    Your order is being processed...
    
  8. Check bot terminal output:

    2025-11-04T10:16:00.000Z  INFO webapp_bot_example: Order from user 123456789: Whopper ($5.99)
    

Success! Complete integration working!

Data flow:

User clicks Order
    ↓
WebApp sends JSON
    ↓
Telegram delivers to bot
    ↓
Bot parses order
    ↓
Bot sends confirmation
    ↓
User sees message

Part 4: Development Workflow

Now you have everything running. Here's the recommended workflow:

4.1 Three Terminal Setup

Terminal 1: WebApp Development Server

cd ~/Projects/telegram-webapp-sdk/demo
trunk serve
  • Compiles Rust to WASM
  • Auto-reloads on code changes
  • Keep running always

Terminal 2: HTTPS Tunnel

cloudflared tunnel --url http://localhost:8080
  • Exposes localhost to internet
  • Provides HTTPS URL for Telegram
  • Keep running always

Terminal 3: Bot Backend

cd ~/Projects/telegram-webapp-sdk/examples/bots/rust_bot
cargo run --release
  • Receives WebApp data
  • Processes orders
  • Keep running always

4.2 Making Changes to WebApp

Edit demo code:

# Open demo source file
code ~/Projects/telegram-webapp-sdk/demo/src/pages/burger_king.rs

Make a change:

// Change price
MenuItem {
    id: 1,
    name: "Whopper",
    price_cents: 699  // Changed from 599
}

Save file

Trunk automatically:

  1. Detects file change
  2. Recompiles WASM
  3. Reloads browser

Check terminal 1:

2025-11-04T10:20:00.000Z INFO 📦 starting build
2025-11-04T10:20:15.000Z INFO ✅ success
2025-11-04T10:20:15.000Z INFO 📡 serving on http://127.0.0.1:8080

Test change:

  1. Refresh WebApp in Telegram
  2. Price should now show $6.99

4.3 Making Changes to Bot

Edit bot code:

code ~/Projects/telegram-webapp-sdk/examples/bots/rust_bot/src/main.rs

Make a change:

// Change response message
let response = format!(
    "🎉 Order Confirmed!\n\n\  // Changed emoji
     Item: {}\n\
     Price: ${:.2}\n\
     Order ID: #{}\n\n\
     Your order will arrive in 30 minutes!",  // Added delivery time
    order.name, price_dollars, order.id
);

Save file

Restart bot:

  1. Press Ctrl+C in terminal 3 to stop bot
  2. Run cargo run --release to restart

Faster restarts with cargo watch:

# Install cargo-watch
cargo install cargo-watch

# Run bot with auto-restart
cargo watch -x 'run --release'

Now bot restarts automatically on code changes!


4.4 Debugging Tips

View WebApp Console:

Since Telegram's in-app browser doesn't show console, use these methods:

Method 1: Desktop Telegram with DevTools

  1. Use Telegram Desktop (not mobile)
  2. Right-click in WebApp
  3. Select "Inspect Element"
  4. Console tab shows logs

Method 2: Test in Regular Browser First

# Open in browser
open http://localhost:8080

# Or
firefox http://localhost:8080

Check browser console (F12) for errors.

Method 3: Use telegram-webapp-sdk Logger

use telegram_webapp_sdk::logger;

logger::info("Button clicked");
logger::error(&format!("Error: {:?}", err));

Logs appear in browser console.


View Bot Logs:

Bot logs appear in terminal 3:

2025-11-04T10:16:00.000Z  INFO webapp_bot_example: Starting...
2025-11-04T10:16:01.000Z  INFO teloxide::dispatching: Listening for updates
2025-11-04T10:16:30.000Z  INFO webapp_bot_example: Order from user 123456789: Whopper ($5.99)

Increase log detail:

Edit .env:

RUST_LOG=debug  # More detailed
RUST_LOG=trace  # Very detailed

Restart bot to apply.


4.5 Common Development Tasks

Update dependencies:

# In demo directory
cd ~/Projects/telegram-webapp-sdk/demo
cargo update

# In bot directory
cd ~/Projects/telegram-webapp-sdk/examples/bots/rust_bot
cargo update

Clean build (if issues):

# Remove build artifacts
cargo clean

# Rebuild from scratch
trunk build --release

Check for errors without running:

cargo check

Faster than full build, finds compilation errors.


Part 5: Stopping Everything

When done developing:

Stop WebApp Server (Terminal 1):

Press: Ctrl+C

Stop Tunnel (Terminal 2):

Press: Ctrl+C

Stop Bot (Terminal 3):

Press: Ctrl+C

Restart later:

Just run the same commands again:

# Terminal 1
trunk serve

# Terminal 2
cloudflared tunnel --url http://localhost:8080

# Terminal 3
cargo run --release

Note: Tunnel URL changes each time, update BotFather if needed.

Production Build

1. Build Optimized WASM

cd demo
trunk build --release --public-url "https://your-domain.com/demo/"

2. Deploy dist/ Contents

ls -lh dist/
# index.html
# demo-*.wasm
# demo-*.js
# style-*.css

# Upload to web server
rsync -av dist/ user@server:/var/www/html/demo/

3. Update Bot Environment

# Update WEBAPP_URL in .env
WEBAPP_URL=https://your-domain.com/demo/index.html

# Restart bot
cargo run --release

4. Update BotFather

Set menu button URL to: https://your-domain.com/demo/index.html

Local Testing with Mock

Enable Mock Feature:

cd demo
# Mock is already enabled in Cargo.toml

Add Mock Initialization:

// In src/main.rs
use telegram_webapp_sdk::mock::{init_mock, MockConfig, MockUser};

telegram_app!(
    pub fn main() -> Result<(), JsValue> {
        // Initialize mock environment
        #[cfg(feature = "mock")]
        init_mock(MockConfig {
            user: Some(MockUser {
                id: 12345,
                first_name: "Test User".into(),
                last_name: Some("Developer".into()),
                username: Some("testdev".into()),
                language_code: Some("en".into()),
                is_premium: Some(true),
                ..Default::default()
            }),
            platform: "web",
            version: "7.0",
            ..Default::default()
        });

        setup_dev_menu();
        telegram_router!();
        Ok(())
    }
);

Test Locally:

trunk serve --open

Now you can test without Telegram or tunneling!


Troubleshooting

Issue: "ReferenceError: Telegram is not defined"

Cause: Telegram WebApp SDK script not loaded.

Solution:

Ensure index.html has:

<head>
    <script src="https://telegram.org/js/telegram-web-app.js"></script>
    <!-- Other scripts below -->
</head>

Issue: wasm-bindgen Version Mismatch

Error:

it looks like the Rust project used to create this wasm file was linked against
version of wasm-bindgen that uses a different bindgen format than this binary:

  rust Wasm file schema version: 0.2.95
   this binary schema version: 0.2.104

Solution:

# Find version in Cargo.lock
grep -A1 'name = "wasm-bindgen"' Cargo.lock | grep version

# Install matching CLI
cargo install wasm-bindgen-cli --version 0.2.95 --force

# Or update dependencies
cargo update
trunk build --release

Issue: WebApp Doesn't Load in Telegram

Possible Causes:

  1. Not HTTPS - Telegram requires secure connection

    • Solution: Use cloudflared/ngrok or deploy to HTTPS server
  2. Wrong URL in BotFather - URL doesn't match deployment

    • Solution: Update menu button URL in BotFather
  3. CORS Issues - Server blocking Telegram's requests

    • Solution: Add CORS headers to server:
      Access-Control-Allow-Origin: https://web.telegram.org
      
  4. Build Path Mismatch - --public-url doesn't match server path

    • Solution: Rebuild with correct URL:
      trunk build --release --public-url "https://domain.com/correct/path/"

Issue: Bot Doesn't Receive Data

Possible Causes:

  1. Bot Not Running - Server stopped

    • Solution: Check bot process and restart
  2. Wrong Token - Invalid TELOXIDE_TOKEN

    • Solution: Verify token from @BotFather
  3. Parsing Error - JSON format mismatch

    • Solution: Check OrderData structure matches WebApp payload
    • Add logging to see actual JSON:
      tracing::info!("Received data: {}", web_app_data.data);
  4. Handler Not Registered - Update dispatcher missing web_app_data filter

    • Solution: Ensure dispatcher has:
      .branch(
          dptree::filter(|msg: Message| msg.web_app_data().is_some())
              .endpoint(handle_webapp_data)
      )

Issue: Dev Menu Buttons Don't Work

Cause: Buttons not connected to handlers.

Solution:

Ensure setup_dev_menu() is called in main:

telegram_app!(
    pub fn main() -> Result<(), JsValue> {
        setup_dev_menu();  // Must be before router
        telegram_router!();
        Ok(())
    }
);

Issue: Large WASM Bundle Size

Optimization Steps:

  1. Enable Release Optimizations in Cargo.toml:

    [profile.release]
    opt-level = "z"
    lto = true
    codegen-units = 1
    strip = true
    panic = "abort"
  2. Install wasm-opt:

    # macOS
    brew install binaryen
    
    # Ubuntu/Debian
    sudo apt-get install binaryen
  3. Use wasm-opt in Build:

    trunk build --release
    wasm-opt -Oz -o dist/demo_bg.wasm dist/demo_bg.wasm
  4. Remove Unused Dependencies:

    cargo install cargo-bloat
    cargo bloat --release --target wasm32-unknown-unknown

Issue: Tunnel URL Changes on Restart

Cause: Free tunneling services generate random URLs.

Solutions:

  1. cloudflared: Use named tunnel (requires account)
  2. ngrok: Use authtoken for persistent URLs
  3. Production: Deploy to permanent HTTPS domain

Best Practice for Development:

Create shell script to update BotFather automatically:

#!/bin/bash
# update_url.sh

TUNNEL_URL=$(curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url')
echo "Tunnel URL: $TUNNEL_URL"
echo "Update this in BotFather manually"
echo "Or use Telegram Bot API to set menu button"

Summary

This chapter provided a complete deep dive into the telegram-webapp-sdk demo and examples:

  1. Architecture: Understood frontend WebApp and backend Bot separation
  2. Demo Structure: Explored complete project layout and file organization
  3. Entry Point: Learned how telegram_app! initializes the SDK
  4. Routing: Understood client-side routing without page reloads
  5. Pages: Analyzed each demo page in detail with code explanations
  6. Components: Studied reusable components and their implementations
  7. Bot Backend: Configured teloxide bot to receive WebApp data
  8. Data Flow: Traced complete order flow from click to confirmation
  9. Testing: Set up local development with tunneling
  10. Troubleshooting: Solved common issues with detailed solutions

Key Takeaways:

  • WebApp is frontend-only, bot backend is required for data processing
  • telegram_app! macro handles SDK initialization automatically
  • telegram_page! macro registers routes with inventory system
  • send_data() sends JSON to bot, which parses it on backend
  • Mock feature enables local testing without Telegram
  • HTTPS is required for production deployment
  • teloxide provides clean async API for bot development

Next Steps:

Apply these concepts to build your own Telegram Mini App:

  1. Clone demo as starting point
  2. Modify pages for your use case
  3. Define custom data structures
  4. Implement bot handlers
  5. Deploy to production

References


Home | Chapter 9: Testing

Clone this wiki locally