skills/telegram-mini-apps/SKILL.md
Telegram Mini Apps development - use for building Mini App frontend, WebApp API, initData authentication, and Telegram integration
npx skillsauth add andvl1/claude-plugin telegram-mini-appsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
// Initialize app
Telegram.WebApp.ready();
Telegram.WebApp.expand();
// Get user data (UNSAFE - for display only)
const user = Telegram.WebApp.initDataUnsafe.user;
// { id, firstName, lastName, username, languageCode, isPremium }
// Get signed data (send to backend for validation)
const initDataRaw = Telegram.WebApp.initData;
// Send via: Authorization: tma ${initDataRaw}
| Method | Purpose |
|--------|---------|
| ready() | Signal app loaded |
| expand() | Expand to full height |
| close() | Close the app |
| sendData(data) | Send data to bot (closes app) |
| openLink(url) | Open external URL |
| openTelegramLink(url) | Open Telegram link |
| showAlert(msg) | Show native alert |
| showConfirm(msg) | Show native confirm |
| showPopup(params) | Show custom popup |
| setHeaderColor(color) | Change header color |
| setBackgroundColor(color) | Change background |
| triggerHapticFeedback(type, style) | Vibration feedback |
const mainButton = Telegram.WebApp.MainButton;
mainButton.text = 'Save Settings';
mainButton.color = '#5288c1';
mainButton.textColor = '#ffffff';
mainButton.show();
mainButton.onClick(() => {
// Handle click
mainButton.showProgress();
// ... async operation
mainButton.hideProgress();
});
const backButton = Telegram.WebApp.BackButton;
backButton.show();
backButton.onClick(() => {
// Navigate back
});
backButton.hide();
const theme = Telegram.WebApp.themeParams;
// {
// bg_color, text_color, hint_color, link_color,
// button_color, button_text_color, secondary_bg_color,
// header_bg_color, accent_text_color, section_bg_color,
// section_header_text_color, subtitle_text_color,
// destructive_text_color
// }
// CSS variables available:
// var(--tg-theme-bg-color)
// var(--tg-theme-text-color)
// etc.
// Theme changed
Telegram.WebApp.onEvent('themeChanged', () => {
const newTheme = Telegram.WebApp.themeParams;
});
// Viewport changed
Telegram.WebApp.onEvent('viewportChanged', () => {
const height = Telegram.WebApp.viewportHeight;
});
// Main button clicked
Telegram.WebApp.onEvent('mainButtonClicked', () => {
// Handle
});
// Back button clicked
Telegram.WebApp.onEvent('backButtonClicked', () => {
// Navigate
});
// Device Storage (5MB per bot)
await Telegram.WebApp.DeviceStorage.setItem('key', 'value');
const value = await Telegram.WebApp.DeviceStorage.getItem('key');
// Secure Storage (encrypted, 10 items max)
await Telegram.WebApp.SecureStorage.setItem('token', 'secret');
// Cloud Storage (synced across devices)
await Telegram.WebApp.CloudStorage.setItem('preference', 'dark');
npm install @telegram-apps/sdk @telegram-apps/sdk-react
import { SDKProvider, useLaunchParams } from '@telegram-apps/sdk-react';
function App() {
return (
<SDKProvider acceptCustomStyles debug>
<Router>
<AppContent />
</Router>
</SDKProvider>
);
}
import {
useLaunchParams,
useInitData,
useInitDataRaw,
useThemeParams,
useViewport,
useMainButton,
useBackButton,
useCloudStorage,
} from '@telegram-apps/sdk-react';
function MyComponent() {
// User data
const initData = useInitData();
const user = initData?.user;
// Raw data for backend
const initDataRaw = useInitDataRaw();
// Theme
const themeParams = useThemeParams();
// Viewport
const viewport = useViewport();
const { height, stableHeight, isExpanded } = viewport;
// Main button
const mainButton = useMainButton();
mainButton.setParams({
text: 'Save',
isVisible: true,
});
mainButton.on('click', handleSave);
}
import { mockTelegramEnv } from '@telegram-apps/sdk-react';
if (import.meta.env.DEV) {
mockTelegramEnv({
themeParams: {
bgColor: '#17212b',
textColor: '#f5f5f5',
buttonColor: '#5288c1',
buttonTextColor: '#ffffff',
// ... other params
},
initData: {
user: {
id: 99281932,
firstName: 'Test',
lastName: 'User',
username: 'testuser',
languageCode: 'en',
isPremium: true,
},
hash: 'mock-hash',
authDate: new Date(),
},
version: '7.2',
platform: 'tdesktop',
});
}
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@Service
class TelegramAuthService(
@Value("\${telegram.bot.token}") private val botToken: String
) {
fun validateInitData(initDataRaw: String): TelegramUser? {
val params = parseInitData(initDataRaw)
val hash = params["hash"] ?: return null
val dataCheckString = params
.filterKeys { it != "hash" }
.toSortedMap()
.map { "${it.key}=${it.value}" }
.joinToString("\n")
val secretKey = hmacSha256("WebAppData".toByteArray(), botToken.toByteArray())
val calculatedHash = hmacSha256(secretKey, dataCheckString.toByteArray())
.toHexString()
if (calculatedHash != hash) return null
val authDate = params["auth_date"]?.toLongOrNull() ?: return null
if (System.currentTimeMillis() / 1000 - authDate > 3600) return null
return parseUser(params["user"] ?: return null)
}
private fun hmacSha256(key: ByteArray, data: ByteArray): ByteArray {
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(key, "HmacSHA256"))
return mac.doFinal(data)
}
}
@RestController
@RequestMapping("/api/v1/miniapp")
class MiniAppController(
private val authService: TelegramAuthService,
private val settingsService: ChatSettingsService
) {
@GetMapping("/chats")
fun getUserChats(
@RequestHeader("Authorization") authHeader: String
): ResponseEntity<List<ChatResponse>> {
val initData = authHeader.removePrefix("tma ")
val user = authService.validateInitData(initData)
?: throw UnauthorizedException("Invalid initData")
val chats = settingsService.findChatsWhereAdmin(user.id)
return ResponseEntity.ok(chats.map { it.toResponse() })
}
@PutMapping("/chats/{chatId}/settings")
fun updateChatSettings(
@PathVariable chatId: Long,
@RequestHeader("Authorization") authHeader: String,
@RequestBody request: UpdateSettingsRequest
): ResponseEntity<ChatSettingsResponse> {
val user = validateAuth(authHeader)
if (!adminService.isAdmin(user.id, chatId)) {
throw ForbiddenException("Not admin in this chat")
}
val settings = settingsService.update(chatId, request)
return ResponseEntity.ok(settings.toResponse())
}
}
npm install @telegram-apps/ui
import {
AppRoot,
Button,
Cell,
Section,
Switch,
Input,
Select,
Slider,
Spinner,
Placeholder,
} from '@telegram-apps/ui';
function SettingsPage() {
return (
<AppRoot>
<Section header="Chat Settings">
<Cell
after={<Switch checked={isEnabled} onChange={setIsEnabled} />}
description="Enable message collection"
>
Collection
</Cell>
<Cell
after={<Select value={action} onChange={setAction}>
<option value="WARN">Warn</option>
<option value="MUTE">Mute</option>
<option value="BAN">Ban</option>
</Select>}
description="Action when warning threshold reached"
>
Threshold Action
</Cell>
<Cell description="Maximum warnings before action">
<Slider
min={1}
max={10}
value={maxWarnings}
onChange={setMaxWarnings}
/>
</Cell>
</Section>
<Button size="large" stretched onClick={handleSave}>
Save Settings
</Button>
</AppRoot>
);
}
sendData() limited to 4096 bytestesting
Android WorkManager for guaranteed background execution - use for deferred tasks, periodic syncs, file uploads, notifications, and task chains. Covers CoroutineWorker, constraints, chaining, testing, and troubleshooting. Use when implementing background work that needs reliable execution across app restarts and doze mode.
tools
Systematic feature planning workflow - use when starting complex features requiring structured approach
development
React 18+ with Vite patterns - use for Mini App frontend development, component structure, hooks, and TypeScript setup
testing
Publish E2E/QA test reports (markdown + screenshots) as secret GitHub Gists. Uses two-gist pattern to work around GitHub rendering limits. Trigger when: report needs to be shared via gist, E2E test run completed and report must be published, user asks to "upload report", "publish to gist", "share test results", or after manual-qa produces a report with screenshots.