Оформлення замовлення у 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

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.