feat(pwa): на экране только включение уведомлений, выключения нет
Строка предлагает включить, пока уведомления выключены, и исчезает, когда они включены. Кнопка «выключить» рядом с балансом только подталкивала бы отказаться от двух событий, ради которых приложение и открывают. Отключение при этом никуда не делось: оно осталось системным — в настройках уведомлений телефона или в настройках сайта в браузере, там же, где человек управляет всеми остальными приложениями. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 { useEffect, useState } from "react";
|
||||||
import { Bell, BellOff } from "lucide-react";
|
import { Bell } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
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() {
|
export function NotificationsRow() {
|
||||||
const [state, setState] = useState<PushState | null>(null);
|
const [state, setState] = useState<PushState | null>(null);
|
||||||
@@ -20,14 +27,15 @@ export function NotificationsRow() {
|
|||||||
return () => { alive = false; };
|
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);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
setState(state === "on" ? await disable() : await enable());
|
setState(await enable());
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Не удалось изменить настройку уведомлений");
|
toast.error("Не удалось включить уведомления");
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -53,10 +61,9 @@ export function NotificationsRow() {
|
|||||||
<button
|
<button
|
||||||
className="flex items-center justify-center gap-1.5 text-muted text-xs mt-6 w-full"
|
className="flex items-center justify-center gap-1.5 text-muted text-xs mt-6 w-full"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={toggle}
|
onClick={turnOn}
|
||||||
>
|
>
|
||||||
{state === "on" ? <BellOff size={15} /> : <Bell size={15} />}
|
<Bell size={15} /> Включить уведомления
|
||||||
{state === "on" ? "Выключить уведомления" : "Включить уведомления"}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user