267 lines
10 KiB
TypeScript
267 lines
10 KiB
TypeScript
"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>
|
||
</>
|
||
);
|
||
}
|