Corp/frontend/src/components/messages/MessagesInstallButton.tsx

267 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { Check, Download, Monitor, Share2, Smartphone } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { usePwa } from "@/contexts/PwaContext";
import { useWebPush } from "@/hooks/useWebPush";
import { apiClient } from "@/lib/api";
import {
clearPendingMessengerRoute,
getDeviceGroupId,
getPendingMessengerRoute,
isMessengerLaunch,
savePendingMessengerRoute,
type NotificationDestination,
} from "@/lib/notifications/installations";
import { Modal } from "@/components/ui";
type DeliveryChoice = "messenger" | "both" | "main";
const CHOICES: Array<{
value: DeliveryChoice;
title: string;
description: string;
destinations: NotificationDestination[];
}> = [
{
value: "messenger",
title: "Только мессенджер",
description: "Без дубликатов в основном приложении на этом устройстве.",
destinations: ["messenger"],
},
{
value: "both",
title: "Оба приложения",
description: "Сообщения будут приходить в CORP и мессенджер.",
destinations: ["main", "messenger"],
},
{
value: "main",
title: "Только CORP",
description: "Мессенджер установится без push о сообщениях.",
destinations: ["main"],
},
];
function isIosDevice() {
if (typeof navigator === "undefined") return false;
return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
}
export default function MessagesInstallButton() {
const {
canInstallMessenger,
installMessenger,
} = usePwa();
const messengerLaunch = useMemo(() => isMessengerLaunch(), []);
const pendingRoute = useMemo(() => getPendingMessengerRoute(), []);
const [open, setOpen] = useState(Boolean(messengerLaunch && pendingRoute));
const [choice, setChoice] = useState<DeliveryChoice>("messenger");
const [showIosInstructions, setShowIosInstructions] = useState(false);
const [finishing, setFinishing] = useState(false);
const [messengerConnected, setMessengerConnected] = useState(false);
const messengerPush = useWebPush({
appKind: "messenger",
enabled: messengerLaunch || open,
});
useEffect(() => {
if (messengerLaunch && getPendingMessengerRoute()) {
setOpen(true);
}
}, [messengerLaunch]);
useEffect(() => {
let mounted = true;
void apiClient.getNotificationDeliverySettings(getDeviceGroupId())
.then((settings) => {
if (!mounted) return;
setMessengerConnected(settings.installations.some(
(installation) => installation.app_kind === "messenger" && installation.installed && installation.push_ready,
));
})
.catch(() => undefined);
return () => {
mounted = false;
};
}, []);
const finishSetup = async (destinations: NotificationDestination[]) => {
setFinishing(true);
try {
const subscribed = messengerPush.isSubscribed || await messengerPush.subscribe();
if (!subscribed) return;
const deviceGroupId = getDeviceGroupId();
const current = await apiClient.getNotificationDeliverySettings(deviceGroupId);
await apiClient.updateNotificationDeliverySettings({
device_group_id: deviceGroupId,
routes: {
...current.routes,
messages: destinations,
},
});
clearPendingMessengerRoute();
setMessengerConnected(true);
setOpen(false);
toast.success("Мессенджер подключен к уведомлениям");
} catch (error) {
console.error(error);
toast.error("Не удалось завершить настройку уведомлений");
} finally {
setFinishing(false);
}
};
const startInstall = async () => {
const destinations = CHOICES.find((item) => item.value === choice)?.destinations || ["messenger"];
savePendingMessengerRoute(destinations);
const result = await installMessenger();
if (result === "accepted") {
setOpen(false);
toast.success("Откройте установленный мессенджер, чтобы включить уведомления");
return;
}
if (result === "dismissed") {
toast.info("Установка отменена. Настройки CORP не изменены");
return;
}
setShowIosInstructions(true);
};
if (!messengerLaunch && (messengerConnected || !canInstallMessenger)) {
return null;
}
return (
<>
<button
type="button"
onClick={() => setOpen(true)}
className="app-action-ghost inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md"
title="Установить мессенджер"
aria-label="Установить мессенджер"
>
<Download size={17} />
</button>
<Modal
isOpen={open}
onClose={() => setOpen(false)}
title={messengerLaunch ? "Завершить настройку" : "Установить мессенджер"}
size="sm"
footer={
<div className="flex w-full justify-end gap-2">
{showIosInstructions ? (
<button
type="button"
onClick={() => setOpen(false)}
className="app-action-primary rounded-lg px-4 py-2 text-sm"
>
Понятно
</button>
) : (
<>
<button
type="button"
onClick={() => setOpen(false)}
className="app-action-secondary rounded-lg px-4 py-2 text-sm"
>
Отмена
</button>
<button
type="button"
onClick={() => {
const destinations = pendingRoute || CHOICES.find((item) => item.value === choice)?.destinations || ["messenger"];
if (messengerLaunch) {
void finishSetup(destinations);
} else {
void startInstall();
}
}}
disabled={finishing || messengerPush.isLoading}
className="app-action-primary inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm disabled:opacity-50"
>
<Download size={16} />
{messengerLaunch ? "Включить уведомления" : "Продолжить"}
</button>
</>
)}
</div>
}
>
{showIosInstructions ? (
<div className="space-y-4 pb-5">
<div className="app-selected rounded-xl p-4">
<div className="flex items-start gap-3">
<Share2 size={20} className="app-accent-text mt-0.5 shrink-0" />
<div>
<p className="text-sm font-semibold text-[var(--foreground)]">
{isIosDevice() ? "Установка на iPhone или iPad" : "Завершите установку мессенджера"}
</p>
<p className="app-text-muted mt-1 text-sm">
{isIosDevice()
? "Нажмите «Поделиться» в браузере, выберите «На экран Домой», затем откройте установленный мессенджер."
: "Используйте команду установки в меню браузера, затем откройте CORP Мессенджер из списка приложений."}
</p>
</div>
</div>
</div>
<p className="app-text-muted text-sm">
Настройки обычного CORP останутся активными, пока мессенджер не запустится и не получит разрешение на уведомления.
</p>
</div>
) : messengerLaunch ? (
<div className="space-y-4 pb-5">
<div className="app-selected rounded-xl p-4">
<div className="flex items-start gap-3">
<Smartphone size={20} className="app-accent-text mt-0.5 shrink-0" />
<div>
<p className="text-sm font-semibold text-[var(--foreground)]">Разрешите системные уведомления</p>
<p className="app-text-muted mt-1 text-sm">
После подтверждения выбранный маршрут сообщений начнет действовать на этом устройстве.
</p>
</div>
</div>
</div>
</div>
) : (
<div className="space-y-4 pb-5">
<p className="app-text-muted text-sm">
Выберите, где получать уведомления о сообщениях на этом устройстве.
</p>
<div className="space-y-2">
{CHOICES.map((item) => {
const selected = choice === item.value;
return (
<button
key={item.value}
type="button"
onClick={() => setChoice(item.value)}
className={`flex w-full items-start gap-3 rounded-xl p-3 text-left transition ${selected ? "app-selected" : "app-surface-muted"}`}
>
<span className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${selected ? "app-badge app-badge-accent" : "app-badge"}`}>
{selected ? <Check size={15} /> : item.value === "main" ? <Monitor size={15} /> : <Smartphone size={15} />}
</span>
<span className="min-w-0">
<span className="block text-sm font-semibold text-[var(--foreground)]">{item.title}</span>
<span className="app-text-muted mt-1 block text-xs">{item.description}</span>
</span>
</button>
);
})}
</div>
{isIosDevice() ? (
<p className="app-text-muted text-xs">На iOS после выбора появится инструкция добавления на домашний экран.</p>
) : null}
</div>
)}
</Modal>
</>
);
}