Оформление заказа в Vue 3 Mini App: корзина в Pinia, синхронизация с localStorage и cURL-оплата на PHP

Создание веб-приложений (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)

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.