admin/services/push.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

126 lines
3.9 KiB
TypeScript

import type { PushSubscriptionResponseDto, PushVapidPublicKeyResponseDto } from '@/api/generated/models'
import {
createServiceError,
errorResult,
handleServiceError,
type ServiceCallOptions,
type ServiceResult,
shouldBubbleErrorToParent,
successResult,
} from '@/services/errorHandler'
import { unwrapApiData } from '@/services/apiResponse'
import { getNotifications } from '@/api/generated/notifications/notifications'
import { clearStoredPushSubscriptionId, getStoredPushSubscriptionId, unsubscribeBrowserPushManager } from '@/lib/pwa'
import { texts } from '@/texts'
export type PushSubscriptionRecord = PushSubscriptionResponseDto
export type VapidPublicKeyResult = PushVapidPublicKeyResponseDto
const notificationsApi = getNotifications()
export const GET_VAPID_PUBLIC_KEY = async (options?: ServiceCallOptions): Promise<ServiceResult<VapidPublicKeyResult>> => {
try {
const res = await notificationsApi.notificationsControllerVapidPublicKey()
if (!res.data.success) throw createServiceError(res.data.message)
const payload = unwrapApiData(res.data)
return successResult({
publicKey: String(payload?.publicKey ?? ''),
enabled: Boolean(payload?.enabled),
})
} catch (error) {
const normalizedError = handleServiceError(error, options)
if (shouldBubbleErrorToParent(options)) {
throw normalizedError
}
return errorResult(normalizedError)
}
}
export const SUBSCRIBE_PUSH = async (
data: { endpoint: string; p256dh: string; auth: string; userAgent?: string },
options?: ServiceCallOptions
): Promise<ServiceResult<PushSubscriptionRecord>> => {
try {
const res = await notificationsApi.notificationsControllerSubscribePush(data)
if (!res.data.success) throw createServiceError(res.data.message)
const payload = unwrapApiData(res.data)
if (!payload?.id) {
throw createServiceError(texts.common.invalidServerResponse)
}
return successResult(payload)
} catch (error) {
const normalizedError = handleServiceError(error, options)
if (shouldBubbleErrorToParent(options)) {
throw normalizedError
}
return errorResult(normalizedError)
}
}
export const UNSUBSCRIBE_PUSH = async (
subscriptionId: string,
options?: ServiceCallOptions
): Promise<ServiceResult<{ deleted: boolean }>> => {
try {
await notificationsApi.notificationsControllerUnsubscribePush(subscriptionId)
return successResult({ deleted: true })
} catch (error) {
const normalizedError = handleServiceError(error, options)
if (shouldBubbleErrorToParent(options)) {
throw normalizedError
}
return errorResult(normalizedError)
}
}
export const UNSUBSCRIBE_ALL_PUSH = async (options?: ServiceCallOptions): Promise<ServiceResult<{ deletedCount: number }>> => {
try {
const res = await notificationsApi.notificationsControllerUnsubscribeAllPush()
if (!res.data.success) throw createServiceError(res.data.message)
const payload = unwrapApiData(res.data)
return successResult({ deletedCount: Number(payload?.deletedCount ?? 0) })
} catch (error) {
const normalizedError = handleServiceError(error, options)
if (shouldBubbleErrorToParent(options)) {
throw normalizedError
}
return errorResult(normalizedError)
}
}
/**
* Removes push delivery for *this* device only: deletes the server-side
* subscription row (if we have its id) and unsubscribes the browser's
* PushManager. Intended for logout — logging out on one device should not
* silence push notifications on the user's other logged-in devices, which
* UNSUBSCRIBE_ALL_PUSH would do.
*/
export const unsubscribeCurrentDevicePush = async (): Promise<void> => {
const subscriptionId = getStoredPushSubscriptionId()
try {
if (subscriptionId) {
await UNSUBSCRIBE_PUSH(subscriptionId, { errorMode: 'silent' })
}
} finally {
await unsubscribeBrowserPushManager()
clearStoredPushSubscriptionId()
}
}