feat(pwa): на экране только включение уведомлений, выключения нет

Строка предлагает включить, пока уведомления выключены, и исчезает, когда они
включены. Кнопка «выключить» рядом с балансом только подталкивала бы отказаться
от двух событий, ради которых приложение и открывают.

Отключение при этом никуда не делось: оно осталось системным — в настройках
уведомлений телефона или в настройках сайта в браузере, там же, где человек
управляет всеми остальными приложениями.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 13:30:19 +10:00
co-authored by Claude Opus 5
parent 4b30b4bbbe
commit 7342a6c9d5
2 changed files with 56 additions and 9 deletions
@@ -0,0 +1,40 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
vi.mock("@/lib/push", () => ({
currentState: vi.fn(async () => "off"),
enable: vi.fn(async () => "on"),
}));
import { NotificationsRow } from "./NotificationsRow";
import * as push from "@/lib/push";
describe("строка уведомлений", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(push.currentState).mockResolvedValue("off");
});
it("выключенные уведомления предлагает включить", async () => {
render(<NotificationsRow />);
await userEvent.click(await screen.findByRole("button", { name: /Включить уведомления/ }));
expect(push.enable).toHaveBeenCalled();
});
it("включённые — строки нет: отказаться от них мы не предлагаем", async () => {
// Отключение остаётся системным (настройки телефона или сайта в браузере) —
// рядом с балансом кнопка «выключить» только подталкивала бы к отказу.
vi.mocked(push.currentState).mockResolvedValue("on");
const { container } = render(<NotificationsRow />);
await vi.waitFor(() => expect(container.textContent).toBe(""));
expect(screen.queryByText(/Выключить/)).not.toBeInTheDocument();
});
it("запрещённые в браузере — объясняем, а не предлагаем кнопку", async () => {
vi.mocked(push.currentState).mockResolvedValue("denied");
render(<NotificationsRow />);
expect(await screen.findByText(/запрещены в настройках браузера/)).toBeInTheDocument();
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
});
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { Bell, BellOff } from "lucide-react";
import { Bell } from "lucide-react";
import { toast } from "sonner";
import { currentState, disable, enable, type PushState } from "@/lib/push";
import { currentState, enable, type PushState } from "@/lib/push";
/**
* Включение уведомлений — одной строкой на главном экране.
@@ -9,6 +9,13 @@ import { currentState, disable, enable, type PushState } from "@/lib/push";
* Не всплывающим окном на входе: разрешение спрашивается один раз за установку,
* и потраченная впустую попытка закрывает канал навсегда. Здесь водитель жмёт
* сам, увидев, ради чего.
*
* Выключения здесь НЕТ намеренно (решение владельца 19.08.2026). Включённые
* уведомления — это ровно те два события, ради которых приложение и открывают;
* кнопка «выключить» рядом с балансом только предлагает от них отказаться.
* Способ отключить остаётся системный: настройки уведомлений телефона или
* настройки сайта в браузере — там же, где человек управляет всеми остальными
* приложениями.
*/
export function NotificationsRow() {
const [state, setState] = useState<PushState | null>(null);
@@ -20,14 +27,15 @@ export function NotificationsRow() {
return () => { alive = false; };
}, []);
if (state === null || state === "unsupported") return null;
// Включено — строки нет: предлагать нечего.
if (state === null || state === "unsupported" || state === "on") return null;
async function toggle() {
async function turnOn() {
setBusy(true);
try {
setState(state === "on" ? await disable() : await enable());
setState(await enable());
} catch {
toast.error("Не удалось изменить настройку уведомлений");
toast.error("Не удалось включить уведомления");
} finally {
setBusy(false);
}
@@ -53,10 +61,9 @@ export function NotificationsRow() {
<button
className="flex items-center justify-center gap-1.5 text-muted text-xs mt-6 w-full"
disabled={busy}
onClick={toggle}
onClick={turnOn}
>
{state === "on" ? <BellOff size={15} /> : <Bell size={15} />}
{state === "on" ? "Выключить уведомления" : "Включить уведомления"}
<Bell size={15} /> Включить уведомления
</button>
);
}