From cd469ae2bb4e3bd34cd2031f97bc29dcd30d388b Mon Sep 17 00:00:00 2001
From: 3252a8 <3252a8@proton.me>
Date: Thu, 28 May 2026 15:54:35 +0300
Subject: [PATCH] feat: add demo device top-up flow
---
docs-site/public/demo/demo-shell.js | 5 +-
docs-site/src/pages/demo.astro | 2 +-
docs/getting-started/demo.md | 2 +-
frontend/src/lib/webapp/mockApi.js | 112 +++++++++++++++++++++++++
frontend/src/lib/webapp/previewMock.js | 13 ++-
5 files changed, 129 insertions(+), 5 deletions(-)
diff --git a/docs-site/public/demo/demo-shell.js b/docs-site/public/demo/demo-shell.js
index f370876..a3bab4e 100644
--- a/docs-site/public/demo/demo-shell.js
+++ b/docs-site/public/demo/demo-shell.js
@@ -72,8 +72,11 @@ const routeFromParams = () => {
return "/home";
};
-const initialMock = normalizeRouteMock(params.get("mock"));
let initialRoute = routeFromParams();
+const mockForRoute = (route) => (normalizePath(route) === "/devices" ? "devices" : "");
+const initialMock = params.has("mock")
+ ? normalizeRouteMock(params.get("mock"))
+ : normalizeRouteMock(mockForRoute(initialRoute) || defaultMock);
if (initialMock === "trial" && initialRoute === "/trial") {
initialRoute = "/home";
const normalizedUrl = new URL(window.location.href);
diff --git a/docs-site/src/pages/demo.astro b/docs-site/src/pages/demo.astro
index 3a9e8f1..c89705b 100644
--- a/docs-site/src/pages/demo.astro
+++ b/docs-site/src/pages/demo.astro
@@ -293,7 +293,7 @@ const docsHref = '/getting-started/demo/';
-
+
diff --git a/docs/getting-started/demo.md b/docs/getting-started/demo.md
index 28c2a32..906b033 100644
--- a/docs/getting-started/demo.md
+++ b/docs/getting-started/demo.md
@@ -11,7 +11,7 @@
- [Админка: пользователи](/demo/admin/users)
- [Админка: бэкапы](/demo/admin/backups)
- [Пробный период](/demo/home?mock=trial)
-- [Устройства](/demo/devices?mock=devices)
+- [Докупка устройств](/demo/devices?mock=devices)
## Как собирается
diff --git a/frontend/src/lib/webapp/mockApi.js b/frontend/src/lib/webapp/mockApi.js
index a6b6945..1954eaa 100644
--- a/frontend/src/lib/webapp/mockApi.js
+++ b/frontend/src/lib/webapp/mockApi.js
@@ -17,7 +17,10 @@ let demoAdsState = null;
let demoSupportTicketsState = null;
let demoSupportMessagesState = null;
let demoTariffsState = null;
+let demoPaymentSequence = 20000;
const demoSettingsChanges = new Map();
+const demoPaymentStatuses = new Map();
+const deviceTopupSaleModes = new Set(["hwid_device", "hwid_devices", "hwid_devices_renewal"]);
function demoPromos() {
if (!demoPromosState) demoPromosState = defaultClone(DEMO_DATASET.promos || []);
@@ -68,6 +71,79 @@ function jsonBody(options) {
}
}
+function isDeviceTopupSaleMode(value) {
+ return deviceTopupSaleModes.has(String(value || "").toLowerCase());
+}
+
+function demoDeviceTopupPlan(body) {
+ const deviceCount = Number(body.device_count || body.months || 0);
+ const plans = DEV_MOCK.data.device_topup_options?.plans || [];
+ return (
+ plans.find(
+ (plan) =>
+ String(plan.tariff_key || "") === String(body.tariff_key || plan.tariff_key || "") &&
+ Number(plan.device_count || plan.purchased_hwid_devices || plan.months || 0) === deviceCount
+ ) ||
+ plans.find(
+ (plan) =>
+ Number(plan.device_count || plan.purchased_hwid_devices || plan.months || 0) === deviceCount
+ ) ||
+ null
+ );
+}
+
+function applyDemoDeviceTopup(deviceCount) {
+ const count = Math.max(1, Number(deviceCount || 0));
+ const subscription = DEV_MOCK.data.subscription || {};
+ const devicesPayload = DEV_MOCK.data.devices || {};
+ const topupOptions = DEV_MOCK.data.device_topup_options || {};
+ const devices = Array.isArray(devicesPayload.devices) ? devicesPayload.devices : [];
+ const currentMax = Number(
+ subscription.max_devices ||
+ devicesPayload.max_devices ||
+ topupOptions.max_devices ||
+ topupOptions.current_limit ||
+ 0
+ );
+ const nextMax = currentMax > 0 ? currentMax + count : currentMax;
+ const currentExtra = Number(
+ subscription.extra_hwid_devices || topupOptions.extra_hwid_devices || 0
+ );
+ const nextExtra = currentExtra + count;
+ const validUntil =
+ subscription.extra_hwid_devices_valid_until_text ||
+ topupOptions.extra_hwid_devices_valid_until_text ||
+ subscription.end_date_text ||
+ "28.06.2026 12:00";
+
+ DEV_MOCK.data.subscription = {
+ ...subscription,
+ active: true,
+ can_topup_devices: true,
+ max_devices: nextMax,
+ extra_hwid_devices: nextExtra,
+ extra_hwid_devices_valid_until_text: validUntil,
+ };
+ DEV_MOCK.data.devices = {
+ ...devicesPayload,
+ ok: true,
+ enabled: true,
+ current_devices: devices.length || Number(devicesPayload.current_devices || 0),
+ max_devices: nextMax,
+ max_devices_label: nextMax > 0 ? String(nextMax) : devicesPayload.max_devices_label || "∞",
+ };
+ DEV_MOCK.data.device_topup_options = {
+ ...topupOptions,
+ ok: true,
+ enabled: true,
+ current_devices: devices.length || Number(topupOptions.current_devices || 0),
+ max_devices: nextMax,
+ current_limit: nextMax,
+ extra_hwid_devices: nextExtra,
+ extra_hwid_devices_valid_until_text: validUntil,
+ };
+}
+
function writeDemoLanguage(language) {
if (typeof window === "undefined") return;
try {
@@ -1681,6 +1757,26 @@ export async function mockApi(path, options = {}, context = {}) {
return { ok: true, csrf_token: "local-preview-csrf" };
}
if (path === "/payments" && String(options.method || "").toUpperCase() === "POST") {
+ const body = jsonBody(options);
+ if (isDeviceTopupSaleMode(body.sale_mode)) {
+ const plan = demoDeviceTopupPlan(body);
+ const deviceCount = Number(
+ body.device_count || plan?.device_count || plan?.purchased_hwid_devices || body.months || 1
+ );
+ const paymentId = ++demoPaymentSequence;
+ demoPaymentStatuses.set(String(paymentId), {
+ status: "pending_yookassa",
+ paid: false,
+ sale_mode: body.sale_mode || "hwid_devices",
+ device_count: deviceCount,
+ applied: false,
+ });
+ return {
+ ok: true,
+ action: "invoice_sent",
+ payment_id: paymentId,
+ };
+ }
return {
ok: true,
action: "open_link",
@@ -1689,6 +1785,22 @@ export async function mockApi(path, options = {}, context = {}) {
};
}
if (/^\/payments\/\d+$/.test(path) && String(options.method || "GET").toUpperCase() === "GET") {
+ const paymentId = String(path.split("/").pop());
+ const status = demoPaymentStatuses.get(paymentId);
+ if (status) {
+ if (!status.applied && isDeviceTopupSaleMode(status.sale_mode)) {
+ applyDemoDeviceTopup(status.device_count);
+ status.applied = true;
+ }
+ status.status = "succeeded";
+ status.paid = true;
+ return {
+ ok: true,
+ payment_id: Number(paymentId),
+ status: status.status,
+ paid: status.paid,
+ };
+ }
return {
ok: true,
payment_id: Number(path.split("/").pop()),
diff --git a/frontend/src/lib/webapp/previewMock.js b/frontend/src/lib/webapp/previewMock.js
index d59eabf..b11d3b9 100644
--- a/frontend/src/lib/webapp/previewMock.js
+++ b/frontend/src/lib/webapp/previewMock.js
@@ -778,31 +778,40 @@ export function applyPreviewMock(kind) {
};
DEV_MOCK.data.device_topup_options = {
ok: true,
+ enabled: true,
+ tariff_key: "standard",
+ tariff_name: "Стандарт",
+ current_limit: 5,
current_devices: 5,
max_devices: 5,
available_extra_devices: 3,
extra_hwid_devices: 0,
+ extra_hwid_devices_valid_until_text: "",
+ renewal_available: false,
+ renewal_recommended_count: 0,
plans: [
{
id: "standard:hwid:1",
tariff_key: "standard",
tariff_name: "Стандарт",
- sale_mode: "hwid_device",
+ sale_mode: "hwid_devices",
purchased_hwid_devices: 1,
price: 120,
currency: "RUB",
title: "+1 устройство",
+ subtitle: "Стандарт",
device_count: 1,
},
{
id: "standard:hwid:3",
tariff_key: "standard",
tariff_name: "Стандарт",
- sale_mode: "hwid_device",
+ sale_mode: "hwid_devices",
purchased_hwid_devices: 3,
price: 290,
currency: "RUB",
title: "+3 устройства",
+ subtitle: "Стандарт",
device_count: 3,
},
],