Створення вебдодатків (Mini Apps) усередині Telegram відкриває потужні можливості для електронної комерції: користувач оформляє покупку, не залишаючи месенджер, а ритейлер отримує високу конверсію завдяки відсутності складної авторизації. Однак під час проєктування користувацького досвіду фронтенд-розробник стикається з низкою специфічних завдань: стійкість кошика при випадкових закриттях WebView, безшовна інтеграція нативного інтерфейсу Telegram (MainButton, HapticFeedback) та безпечна передача складу замовлення на backend для виписування рахунку.
У цій статті ми детально розберемо побудову модуля каталогу на Vue 3 та Pinia зі збереженням стану в localStorage, а також опишемо серверний обробник на PHP, який перевіряє справжність даних і формує платіжне посилання через Telegram Bot API.
Архітектура клієнтського стану: Pinia та localStorage
Під час роботи в контейнері Telegram WebView користувач може випадково змахнути вікно додатка або згорнути месенджер. Якщо зберігати кошик виключно в оперативній пам'яті (Vue reactivity), будь-який збій або перезавантаження вкладки призведуть до втрати вибраних позицій. Це критично знижує конверсію e-commerce проєктів.
Рішенням є гібридний підхід: стан кошика управляється реактивним стором Pinia, а кожна зміна складу або кількості товарів синхронізується з localStorage. Під час ініціалізації додатка стор зчитує збережений JSON і відновлює позицію покупця.
import { defineStore } from 'pinia';
const LOCAL_STORAGE_KEY = 'tma_cart_items_v1';
export const useCartStore = defineStore('cart', {
state: () => ({
items: JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '[]')
}),
getters: {
totalItems: (state) => state.items.reduce((acc, item) => acc + item.quantity, 0),
totalPrice: (state) => state.items.reduce((acc, item) => acc + item.price * item.quantity, 0)
},
actions: {
saveToStorage() {
try {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.items));
} catch (e) {
console.error('Ошибка записи в localStorage', e);
}
},
addItem(product) {
const existing = this.items.find(i => i.id === product.id);
if (existing) {
existing.quantity += 1;
} else {
this.items.push({
id: product.id,
title: product.title,
price: product.price,
quantity: 1
});
}
this.saveToStorage();
},
removeItem(productId) {
this.items = this.items.filter(i => i.id !== productId);
this.saveToStorage();
},
updateQuantity(productId, quantity) {
const item = this.items.find(i => i.id === productId);
if (item) {
item.quantity = quantity;
if (item.quantity <= 0) {
this.removeItem(productId);
return;
}
}
this.saveToStorage();
},
clearCart() {
this.items = [];
localStorage.removeItem(LOCAL_STORAGE_KEY);
}
}
});Зверніть увагу: ціни товарів у сторі рекомендується зберігати в мінімальних грошових одиницях (копійки, центи) у форматі цілих чисел (integer). Це запобігає проблемам із точністю обчислень із плаваючою крапкою (float), характерним для JavaScript та PHP.
Інтеграція з Telegram WebApp SDK та управління MainButton
Інтерфейсний патерн Telegram Mini Apps передбачає використання нативної кнопки MainButton, розташованої в нижній частині екрана месенджера, замість стандартної вебкнопки «Оформити замовлення». Це створює відчуття роботи з нативним мобільним додатком.
Для синхронізації стану кошика та нативної кнопки організовується реактивне відстеження гетера totalItems через Vue watch. Коли кошик не порожній, MainButton відображається з текстом суми замовлення. При кліку на кнопку запускається процес оформлення.
<template>
<div class="catalog">
<h2>Каталог товаров</h2>
<div v-for="product in products" :key="product.id" class="product-item">
<div class="product-info">
<strong>{{ product.title }}</strong>
<span>{{ (product.price / 100).toFixed(2) }} ₽</span>
</div>
<button @click="addToCart(product)