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,