Creating a fully-fledged online store inside Telegram Mini Apps (TMA) requires not only a responsive interface but also a reliable backend architecture. The client application on Vue 3 provides a seamless UX, but you cannot trust the frontend with order cost calculations. Any prices, discounts, and stock levels must undergo strict validation on the server side.
In this article, we will look at creating a reliable hybrid checkout process: from managing a shopping cart in Pinia with state persistence in localStorage to generating invoices via Telegram Bot API (including Telegram Stars) and integrating external payment gateways in PHP.
1. Shopping Cart Architecture on Vue 3: Pinia and LocalStorage
Mini Apps users often close the application or switch to other chats. To prevent the cart from resetting when the WebApp is restarted, the Pinia state must be synchronized with localStorage. Below is a reactive shopping cart that saves state and automatically calculates the total.
// stores/cart.js
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', {
state: () => ({
items: JSON.parse(localStorage.getItem('tma_cart_items')) || []
}),
getters: {
totalAmount: (state) => {
return state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
},
totalQuantity: (state) => {
return state.items.reduce((sum, item) => sum + item.quantity, 0);
}
},
actions: {
addToCart(product) {
const existing = this.items.find(i => i.id === product.id);
if (existing) {
existing.quantity++;
} else {
this.items.push({ ...product, quantity: 1 });
}
this.saveToStorage();
},
updateQuantity(productId, quantity) {
const item = this.items.find(i => i.id === productId);
if (item) {
item.quantity = Math.max(1, quantity);
}
this.saveToStorage();
},
removeFromCart(productId) {
this.items = this.items.filter(i => i.id !== productId);
this.saveToStorage();
},
clearCart() {
this.items = [];
this.saveToStorage();
},
saveToStorage() {
localStorage.setItem('tma_cart_items', JSON.stringify(this.items));
}
}
});2. Cart Validation on the PHP Backend
The main security rule: never trust prices coming from the client. An attacker can change the price of an item in local storage before sending the request. We only send item identifiers (id) and their quantity (quantity) to the backend, along with the initData initialization string to verify the user's authenticity.
Below is an order checkout script that validates prices against the database, generates a unique transaction identifier lead_id using a cryptographically secure function, and sends an invoice to the user via the Bot API.
<?php
// checkout.php
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['items']) || empty($input['initData'])) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Неверные входные данные']);
exit;
}
// Имитация базы данных товаров
$catalogDb = [
101 => ['title' => 'Набор стикеров Premium', 'price' => 500],
102 => ['title' => 'Подписка на закрытый канал', 'price' => 1500],
];
$prices = [];
$totalAmount = 0;
$leadId = bin2hex(random_bytes(7)); // Уникальный ID заказа
foreach ($input['items'] as $item) {
$id = (int)($item['id'] ?? 0);
$qty = (int)($item['quantity'] ?? 0);
if (isset($catalogDb[$id]) && $qty > 0) {
$product = $catalogDb[$id];
$prices[] = [
'label' => $product['title'] . " x{$qty}\