From bc0f01228cc6a124f90b852aaffcaf27e5c949e5 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 9 Mar 2026 14:59:33 +0800 Subject: [PATCH] feat: use api to show notification --- web/app/(commonLayout)/layout.tsx | 2 + .../app/in-site-message-notification.tsx | 100 ++++++++++++++++++ web/contract/console/notification.ts | 24 +++++ web/contract/router.ts | 2 + 4 files changed, 128 insertions(+) create mode 100644 web/app/components/app/in-site-message-notification.tsx create mode 100644 web/contract/console/notification.ts diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index db2786f6cf..222726a4ac 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/web/app/(commonLayout)/layout.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react' import * as React from 'react' import { AppInitializer } from '@/app/components/app-initializer' +import InSiteMessageNotification from '@/app/components/app/in-site-message-notification' import AmplitudeProvider from '@/app/components/base/amplitude' import GA, { GaType } from '@/app/components/base/ga' import Zendesk from '@/app/components/base/zendesk' @@ -32,6 +33,7 @@ const Layout = ({ children }: { children: ReactNode }) => { {children} + diff --git a/web/app/components/app/in-site-message-notification.tsx b/web/app/components/app/in-site-message-notification.tsx new file mode 100644 index 0000000000..a2b6dedbc6 --- /dev/null +++ b/web/app/components/app/in-site-message-notification.tsx @@ -0,0 +1,100 @@ +'use client' + +import type { InSiteMessageActionItem } from './in-site-message' +import { useQuery } from '@tanstack/react-query' +import { useTranslation } from 'react-i18next' +import { IS_CLOUD_EDITION } from '@/config' +import { consoleClient, consoleQuery } from '@/service/client' +import InSiteMessage from './in-site-message' + +type NotificationBodyPayload = { + actions: InSiteMessageActionItem[] + main: string +} + +function isValidActionItem(value: unknown): value is InSiteMessageActionItem { + if (!value || typeof value !== 'object') + return false + + const candidate = value as { + action?: unknown + data?: unknown + text?: unknown + type?: unknown + } + + return ( + typeof candidate.text === 'string' + && (candidate.type === 'primary' || candidate.type === 'default') + && (candidate.action === 'link' || candidate.action === 'close') + && (candidate.data === undefined || typeof candidate.data !== 'function') + ) +} + +function parseNotificationBody(body: string): NotificationBodyPayload | null { + try { + const parsed = JSON.parse(body) as { + actions?: unknown + main?: unknown + } + + if (!parsed || typeof parsed !== 'object') + return null + + if (typeof parsed.main !== 'string') + return null + + const actions = Array.isArray(parsed.actions) + ? parsed.actions.filter(isValidActionItem) + : [] + + return { + main: parsed.main, + actions, + } + } + catch { + return null + } +} + +function InSiteMessageNotification() { + const { t } = useTranslation() + const { data } = useQuery({ + queryKey: consoleQuery.notification.queryKey(), + queryFn: async () => { + return await consoleClient.notification() + }, + enabled: IS_CLOUD_EDITION, + }) + + const notification = data?.notifications?.[0] + const parsedBody = notification ? parseNotificationBody(notification.body) : null + + if (!IS_CLOUD_EDITION || !notification) + return null + + const fallbackActions: InSiteMessageActionItem[] = [ + { + type: 'default', + text: t('operation.close', { ns: 'common' }), + action: 'close', + }, + ] + + const actions = parsedBody?.actions?.length ? parsedBody.actions : fallbackActions + const main = parsedBody?.main ?? 'Invalid notification body' + + return ( + + ) +} + +export default InSiteMessageNotification diff --git a/web/contract/console/notification.ts b/web/contract/console/notification.ts new file mode 100644 index 0000000000..df7e073263 --- /dev/null +++ b/web/contract/console/notification.ts @@ -0,0 +1,24 @@ +import { type } from '@orpc/contract' +import { base } from '../base' + +export type ConsoleNotification = { + body: string + frequency: 'once' | 'always' + lang: string + notification_id: string + subtitle: string + title: string + title_pic_url?: string +} + +export type ConsoleNotificationResponse = { + notifications: ConsoleNotification[] + should_show: boolean +} + +export const notificationContract = base + .route({ + path: '/notification', + method: 'GET', + }) + .output(type()) diff --git a/web/contract/router.ts b/web/contract/router.ts index 79a95be55a..b9ef07fa6a 100644 --- a/web/contract/router.ts +++ b/web/contract/router.ts @@ -12,6 +12,7 @@ import { exploreInstalledAppsContract, exploreInstalledAppUninstallContract, } from './console/explore' +import { notificationContract } from './console/notification' import { systemFeaturesContract } from './console/system' import { triggerOAuthConfigContract, @@ -67,6 +68,7 @@ export const consoleRouterContract = { invoices: invoicesContract, bindPartnerStack: bindPartnerStackContract, }, + notification: notificationContract, triggers: { list: triggersContract, providerInfo: triggerProviderInfoContract,