diff --git a/bot/app/web/frontend/src/App.svelte b/bot/app/web/frontend/src/App.svelte
index de1683b..8a47f8d 100644
--- a/bot/app/web/frontend/src/App.svelte
+++ b/bot/app/web/frontend/src/App.svelte
@@ -17,6 +17,7 @@
Home,
LockKeyhole,
Mail,
+ Plus,
RefreshCw,
Send,
Smartphone,
@@ -215,10 +216,13 @@
let paymentStep = "tariff";
let selectedTariffKey = "";
let topupModalOpen = query.get("topup") === "1";
+ let deviceTopupModalOpen = query.get("device_topup") === "1";
let changeModalOpen = query.get("change") === "1";
let topupOptions = null;
+ let deviceTopupOptions = null;
let changeOptions = null;
let selectedTopupPlan = null;
+ let selectedDeviceTopupPlan = null;
let selectedChangeTarget = null;
let selectedChangeAction = null;
let changeConfirmOpen = false;
@@ -409,6 +413,16 @@
{ id: "standard:topup:200", tariff_key: "standard", tariff_name: "Стандарт", sale_mode: "topup", traffic_gb: 200, months: 200, price: 1299, currency: "RUB", title: "200 GB", subtitle: "Стандарт" },
],
};
+ DEV_MOCK.data.device_topup_options = {
+ ok: true,
+ tariff_key: "standard",
+ tariff_name: "Стандарт",
+ current_limit: 5,
+ plans: [
+ { id: "standard:hwid:1", tariff_key: "standard", tariff_name: "Стандарт", sale_mode: "hwid_devices", device_count: 1, months: 1, price: 99, currency: "RUB", title: "+1", subtitle: "Стандарт" },
+ { id: "standard:hwid:3", tariff_key: "standard", tariff_name: "Стандарт", sale_mode: "hwid_devices", device_count: 3, months: 3, price: 249, currency: "RUB", title: "+3", subtitle: "Стандарт" },
+ ],
+ };
} else if (mode === "devices") {
DEV_MOCK.data.settings.my_devices_enabled = true;
DEV_MOCK.data.subscription = {
@@ -482,7 +496,7 @@
$: telegramLoginBotId = Number(CFG.telegramLoginBotId || 0);
$: telegramOAuthClientId = Number(CFG.telegramOAuthClientId || telegramLoginBotId || 0);
$: applyFavicon(CFG.logoUrl, brandEmoji);
- $: syncBodyScrollLock(paymentModalOpen || changeModalOpen || changeConfirmOpen || topupModalOpen || linkEmailOpen);
+ $: syncBodyScrollLock(paymentModalOpen || changeModalOpen || changeConfirmOpen || topupModalOpen || deviceTopupModalOpen || linkEmailOpen);
$: if (!tariffMode && !selectedPlan && plans.length) selectedPlan = plans[Math.min(1, plans.length - 1)];
$: if (tariffMode && selectedTariffKey && !tariffCatalog.some((tariff) => tariff.key === selectedTariffKey)) {
selectedTariffKey = "";
@@ -753,6 +767,7 @@
await loadDevices();
}
if (topupModalOpen) await loadTopupOptions();
+ if (deviceTopupModalOpen) await loadDeviceTopupOptions();
if (changeModalOpen) await loadTariffChangeOptions();
}
@@ -805,6 +820,7 @@
}
if (path === "/promo/apply") return { ok: true, end_date_text: "31.05.2026" };
if (path === "/devices") return structuredCloneSafe(DEV_MOCK.data.devices);
+ if (path === "/devices/topup-options") return structuredCloneSafe(DEV_MOCK.data.device_topup_options || { ok: true, plans: [] });
if (path === "/tariffs/topup-options") return structuredCloneSafe(DEV_MOCK.data.topup_options || { ok: true, plans: [] });
if (path === "/tariffs/change-options") return structuredCloneSafe(DEV_MOCK.data.tariff_change_options || { ok: true, targets: [] });
if (path === "/devices/disconnect" && String(options.method || "").toUpperCase() === "POST") {
@@ -1327,6 +1343,7 @@
body: JSON.stringify({
months: selectedPlan.months,
traffic_gb: selectedPlan.traffic_gb,
+ device_count: selectedPlan.device_count,
tariff_key: selectedPlan.tariff_key,
sale_mode: selectedPlan.sale_mode,
method: selectedMethod,
@@ -1410,6 +1427,47 @@
}
}
+ async function loadDeviceTopupOptions() {
+ if (deviceTopupOptions || tariffActionBusy) return;
+ tariffActionBusy = true;
+ try {
+ const response = await api("/devices/topup-options");
+ if (!response?.ok) throw response;
+ deviceTopupOptions = response;
+ selectedDeviceTopupPlan = response.plans?.[0] || null;
+ } catch (error) {
+ showToast(error?.message || t("wa_device_topup_options_failed"));
+ deviceTopupModalOpen = false;
+ } finally {
+ tariffActionBusy = false;
+ }
+ }
+
+ async function createDeviceTopupPayment() {
+ if (!selectedDeviceTopupPlan || !selectedMethod || payBusy) return;
+ payBusy = true;
+ try {
+ const response = await api("/payments", {
+ method: "POST",
+ body: JSON.stringify({
+ months: selectedDeviceTopupPlan.device_count || selectedDeviceTopupPlan.months,
+ device_count: selectedDeviceTopupPlan.device_count || selectedDeviceTopupPlan.months,
+ tariff_key: selectedDeviceTopupPlan.tariff_key || deviceTopupOptions?.tariff_key,
+ sale_mode: "hwid_devices",
+ method: selectedMethod,
+ }),
+ });
+ if (!response.ok || !response.payment_url) throw response;
+ showToast(t("wa_payment_created"));
+ openExternalLink(response.payment_url);
+ deviceTopupModalOpen = false;
+ } catch (error) {
+ showToast(error?.message || t("wa_payment_create_failed"));
+ } finally {
+ payBusy = false;
+ }
+ }
+
async function applyTariffChange() {
if (!selectedChangeTarget || !selectedChangeAction || tariffActionBusy) return;
if (selectedChangeAction.kind === "payment") {
@@ -1676,6 +1734,16 @@
loadDevices();
}
+ function openDeviceTopupModal() {
+ selectedMethod = methods[0]?.id || "";
+ deviceTopupModalOpen = true;
+ loadDeviceTopupOptions();
+ }
+
+ function closeDeviceTopupModal() {
+ deviceTopupModalOpen = false;
+ }
+
function goSettings() {
paymentModalOpen = false;
activeTab = "settings";
@@ -2539,6 +2607,12 @@
+ {#if subscription?.active && subscription?.max_devices !== 0}
+
+ {/if}
{#if devicesBusy && !devicesLoaded}
@@ -3092,6 +3166,65 @@
+
+