227 lines
7.7 KiB
Vue
227 lines
7.7 KiB
Vue
|
|
<template>
|
||
|
|
<div class="text-sm">
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<div v-if="isPending" class="w-5 h-5">
|
||
|
|
<ProgressSpinner style="width: 100%; height: 100%" strokeWidth="8" fill="transparent" />
|
||
|
|
</div>
|
||
|
|
<Checkbox v-else v-model="enabled" inputId="webPush" name="delivery" binary />
|
||
|
|
|
||
|
|
<label for="webPush">Показать уведомление, когда придет ответ</label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Инструкции для iOS -->
|
||
|
|
<div
|
||
|
|
v-if="showRequireStandaloneInstructionsForIOS"
|
||
|
|
class="bg-yellow-100 text-yellow-800 p-4 rounded my-4"
|
||
|
|
>
|
||
|
|
<p class="mb-2">Чтобы включить уведомления, добавьте это приложение на главный экран:</p>
|
||
|
|
<ul class="list-disc pl-5">
|
||
|
|
<li class="mb-1">
|
||
|
|
Нажмите
|
||
|
|
<strong class="font-semibold">
|
||
|
|
"<img
|
||
|
|
src="@/assets/icons/apple-share.svg?url"
|
||
|
|
alt="Поделиться"
|
||
|
|
class="h-7 -mt-2 inline-block"
|
||
|
|
/>"
|
||
|
|
</strong>
|
||
|
|
внизу экрана
|
||
|
|
</li>
|
||
|
|
<li class="mb-1">
|
||
|
|
Выберите
|
||
|
|
<strong class="inline-flex items-baseline font-semibold">
|
||
|
|
<span class="me-1">"На экран «Домой»</span>
|
||
|
|
<img
|
||
|
|
src="@/assets/icons/apple-system-plus.svg?url"
|
||
|
|
alt=""
|
||
|
|
class="h-4 inline-block self-center"
|
||
|
|
/>"
|
||
|
|
</strong>
|
||
|
|
</li>
|
||
|
|
<li>Откройте приложение с главного экрана и повторите попытку.</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Ошибка разрешений -->
|
||
|
|
<p v-else-if="isError" class="text-red-500 mb-2">{{ error?.message || error }}<br /></p>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { computed, ref, watch } from 'vue'
|
||
|
|
import { useMutation } from '@tanstack/vue-query'
|
||
|
|
import {
|
||
|
|
getVapidPublicKey,
|
||
|
|
getWebPushSubscriptionDocument,
|
||
|
|
saveWebPushSubscription,
|
||
|
|
} from '@/api/webPush'
|
||
|
|
import { getUserChatSettingsDocument } from '@/api/userChatSettings'
|
||
|
|
import type { UserChatSettingsDocument } from '@hereconnect/types'
|
||
|
|
import { isNotFoundError } from '@/api/PouchDB'
|
||
|
|
import { saveUserChatSettings } from '@/api/userChatSettings'
|
||
|
|
|
||
|
|
// Проверки iOS и запуска из главного экрана
|
||
|
|
const isIOS = computed(
|
||
|
|
() => /iPad|iPhone|iPod/.test(navigator.userAgent) && !('MSStream' in window && window.MSStream),
|
||
|
|
)
|
||
|
|
const isStandalone = computed(() => 'standalone' in window.navigator && window.navigator.standalone)
|
||
|
|
|
||
|
|
const { qr_code_uri, chat, user_chat_role } = defineProps<{
|
||
|
|
qr_code_uri: UserChatSettingsDocument['qr_code_uri']
|
||
|
|
chat: UserChatSettingsDocument['chat']
|
||
|
|
user_chat_role: UserChatSettingsDocument['user_chat_role']
|
||
|
|
}>()
|
||
|
|
|
||
|
|
const enabled = ref<boolean>(false)
|
||
|
|
|
||
|
|
const subscribeOptionsPromise = getVapidPublicKey().then((applicationServerKey) => ({
|
||
|
|
userVisibleOnly: true,
|
||
|
|
applicationServerKey,
|
||
|
|
}))
|
||
|
|
|
||
|
|
// Функция запроса разрешений и подписки
|
||
|
|
async function requestNotificationPermission() {
|
||
|
|
if (!('Notification' in window)) {
|
||
|
|
throw new Error(
|
||
|
|
isIOS.value && !isStandalone.value
|
||
|
|
? 'Добавьте приложение на главный экран, чтобы использовать уведомления.'
|
||
|
|
: 'Уведомления не поддерживаются этим браузером.',
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const permission = await Notification.requestPermission()
|
||
|
|
if (permission !== 'granted') {
|
||
|
|
throw new Error(`Разрешение на уведомления не предоставлено: ${permission}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const registration = await navigator.serviceWorker.ready
|
||
|
|
let subscription: PushSubscription | null = null
|
||
|
|
|
||
|
|
try {
|
||
|
|
subscription = await registration.pushManager.subscribe(await subscribeOptionsPromise)
|
||
|
|
} catch (error) {
|
||
|
|
subscription = await registration.pushManager.getSubscription()
|
||
|
|
if (!subscription) {
|
||
|
|
throw error
|
||
|
|
}
|
||
|
|
await subscription.unsubscribe()
|
||
|
|
subscription = await registration.pushManager.subscribe(await subscribeOptionsPromise)
|
||
|
|
}
|
||
|
|
|
||
|
|
return saveWebPushSubscription(subscription)
|
||
|
|
}
|
||
|
|
|
||
|
|
const getPushSubscription = async () => {
|
||
|
|
try {
|
||
|
|
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||
|
|
const registration = await navigator.serviceWorker.ready
|
||
|
|
|
||
|
|
const permission = await registration.pushManager.permissionState(
|
||
|
|
await subscribeOptionsPromise,
|
||
|
|
)
|
||
|
|
console.debug('permission', permission)
|
||
|
|
if (permission === 'prompt') {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if (permission !== 'granted') {
|
||
|
|
throw new Error(`Разрешение на уведомления не предоставлено: ${permission}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const subscription = await registration.pushManager.getSubscription()
|
||
|
|
if (subscription) {
|
||
|
|
console.debug('Уже есть активная подписка:', subscription)
|
||
|
|
return subscription
|
||
|
|
} else {
|
||
|
|
console.debug('Нет активной подписки на Push-уведомления.')
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
console.warn('Push уведомления или Service Worker не поддерживаются в этом браузере.')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Произошла ошибка при получении подписки:', error)
|
||
|
|
throw error
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const stage = ref<'receivePushSubscription' | 'createPushSubscription' | 'removePushSubscription'>(
|
||
|
|
'receivePushSubscription',
|
||
|
|
)
|
||
|
|
|
||
|
|
// Хуки для состояния запроса
|
||
|
|
const { isPending, isError, error, mutate } = useMutation({
|
||
|
|
mutationKey: [stage],
|
||
|
|
mutationFn: async () => {
|
||
|
|
console.debug(stage.value)
|
||
|
|
try {
|
||
|
|
if (stage.value === 'receivePushSubscription') {
|
||
|
|
const chatSettingsDocument = await getUserChatSettingsDocument({
|
||
|
|
qr_code_uri,
|
||
|
|
chat,
|
||
|
|
user_chat_role,
|
||
|
|
}).catch((error) => {
|
||
|
|
if (!isNotFoundError(error)) throw error
|
||
|
|
})
|
||
|
|
if (chatSettingsDocument && chatSettingsDocument.is_push_notification_enabled) {
|
||
|
|
const [subscription, subscriptionDocument] = await Promise.all([
|
||
|
|
getPushSubscription(),
|
||
|
|
getWebPushSubscriptionDocument().catch((error) => {
|
||
|
|
if (!isNotFoundError(error)) throw error
|
||
|
|
}),
|
||
|
|
])
|
||
|
|
if (subscription) {
|
||
|
|
if (!subscriptionDocument) {
|
||
|
|
await saveWebPushSubscription(subscription)
|
||
|
|
}
|
||
|
|
enabled.value = true
|
||
|
|
stage.value = 'removePushSubscription'
|
||
|
|
} else {
|
||
|
|
stage.value = 'createPushSubscription'
|
||
|
|
enabled.value = false
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
stage.value = 'createPushSubscription'
|
||
|
|
}
|
||
|
|
} else if (stage.value === 'createPushSubscription') {
|
||
|
|
await requestNotificationPermission()
|
||
|
|
await saveUserChatSettings({
|
||
|
|
qr_code_uri,
|
||
|
|
chat,
|
||
|
|
user_chat_role,
|
||
|
|
is_push_notification_enabled: true,
|
||
|
|
})
|
||
|
|
} else if (stage.value === 'removePushSubscription') {
|
||
|
|
await saveUserChatSettings({
|
||
|
|
qr_code_uri,
|
||
|
|
chat,
|
||
|
|
user_chat_role,
|
||
|
|
is_push_notification_enabled: false,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
enabled.value = false
|
||
|
|
throw error
|
||
|
|
}
|
||
|
|
return null
|
||
|
|
},
|
||
|
|
retry: false,
|
||
|
|
})
|
||
|
|
|
||
|
|
watch(
|
||
|
|
[() => stage.value, () => enabled.value],
|
||
|
|
() => {
|
||
|
|
if (stage.value === 'removePushSubscription') {
|
||
|
|
if (!enabled.value) mutate()
|
||
|
|
} else if (stage.value === 'createPushSubscription') {
|
||
|
|
if (enabled.value) mutate()
|
||
|
|
} else {
|
||
|
|
mutate()
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{ immediate: true },
|
||
|
|
)
|
||
|
|
|
||
|
|
const showRequireStandaloneInstructionsForIOS = computed(
|
||
|
|
() => isError.value && isIOS.value && !isStandalone.value && enabled.value,
|
||
|
|
)
|
||
|
|
</script>
|