@l.x/notifications 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.depcheckrc +14 -0
- package/.eslintrc.js +20 -0
- package/LICENSE +122 -0
- package/README.md +548 -0
- package/package.json +42 -1
- package/project.json +24 -0
- package/src/getIsNotificationServiceLocalOverrideEnabled.ts +7 -0
- package/src/global.d.ts +2 -0
- package/src/index.ts +41 -0
- package/src/notification-data-source/NotificationDataSource.ts +10 -0
- package/src/notification-data-source/getNotificationQueryOptions.ts +85 -0
- package/src/notification-data-source/implementations/createIntervalNotificationDataSource.ts +77 -0
- package/src/notification-data-source/implementations/createLocalTriggerDataSource.test.ts +492 -0
- package/src/notification-data-source/implementations/createLocalTriggerDataSource.ts +177 -0
- package/src/notification-data-source/implementations/createNotificationDataSource.ts +19 -0
- package/src/notification-data-source/implementations/createPollingNotificationDataSource.test.ts +398 -0
- package/src/notification-data-source/implementations/createPollingNotificationDataSource.ts +74 -0
- package/src/notification-data-source/implementations/createReactiveDataSource.ts +113 -0
- package/src/notification-data-source/types/ReactiveCondition.ts +60 -0
- package/src/notification-processor/NotificationProcessor.ts +26 -0
- package/src/notification-processor/implementations/createBaseNotificationProcessor.test.ts +854 -0
- package/src/notification-processor/implementations/createBaseNotificationProcessor.ts +254 -0
- package/src/notification-processor/implementations/createNotificationProcessor.test.ts +130 -0
- package/src/notification-processor/implementations/createNotificationProcessor.ts +15 -0
- package/src/notification-renderer/NotificationRenderer.ts +8 -0
- package/src/notification-renderer/components/BannerTemplate.tsx +188 -0
- package/src/notification-renderer/components/InlineBannerNotification.tsx +123 -0
- package/src/notification-renderer/implementations/createNotificationRenderer.ts +16 -0
- package/src/notification-renderer/utils/iconUtils.ts +103 -0
- package/src/notification-service/NotificationService.ts +49 -0
- package/src/notification-service/implementations/createNotificationService.test.ts +1092 -0
- package/src/notification-service/implementations/createNotificationService.ts +368 -0
- package/src/notification-telemetry/NotificationTelemetry.ts +44 -0
- package/src/notification-telemetry/implementations/createNotificationTelemetry.test.ts +99 -0
- package/src/notification-telemetry/implementations/createNotificationTelemetry.ts +33 -0
- package/src/notification-tracker/NotificationTracker.ts +14 -0
- package/src/notification-tracker/implementations/createApiNotificationTracker.test.ts +465 -0
- package/src/notification-tracker/implementations/createApiNotificationTracker.ts +154 -0
- package/src/notification-tracker/implementations/createNoopNotificationTracker.ts +44 -0
- package/src/notification-tracker/implementations/createNotificationTracker.ts +31 -0
- package/src/utils/formatNotificationType.test.ts +25 -0
- package/src/utils/formatNotificationType.ts +25 -0
- package/tsconfig.json +34 -0
- package/tsconfig.lint.json +8 -0
- package/vitest-setup.ts +1 -0
- package/vitest.config.ts +14 -0
- package/index.d.ts +0 -1
- package/index.js +0 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { BackgroundType } from '@luxamm/client-notification-service/dist/lx/notificationservice/v1/api_pb'
|
|
2
|
+
import type { InAppNotification } from '@l.x/api'
|
|
3
|
+
import {
|
|
4
|
+
BannerTemplate,
|
|
5
|
+
type BannerTemplateButton,
|
|
6
|
+
} from '@l.x/notifications/src/notification-renderer/components/BannerTemplate'
|
|
7
|
+
import { parseCustomIconLink } from '@l.x/notifications/src/notification-renderer/utils/iconUtils'
|
|
8
|
+
import { type NotificationClickTarget } from '@l.x/notifications/src/notification-service/NotificationService'
|
|
9
|
+
import { memo, useMemo } from 'react'
|
|
10
|
+
import { useTranslation } from 'react-i18next'
|
|
11
|
+
|
|
12
|
+
const BANNER_ICON_SIZE = 40
|
|
13
|
+
|
|
14
|
+
interface InlineBannerNotificationProps {
|
|
15
|
+
notification: InAppNotification
|
|
16
|
+
onNotificationClick?: (notificationId: string, target: NotificationClickTarget) => void
|
|
17
|
+
/** Override the default banner width. Use '100%' for full-width. */
|
|
18
|
+
width?: number | string
|
|
19
|
+
/** Enable button rendering. Defaults to false (web only). */
|
|
20
|
+
renderButton?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* InlineBannerNotification component
|
|
25
|
+
*
|
|
26
|
+
* A wrapper around BannerTemplate for rendering notification API-driven banners.
|
|
27
|
+
* Can be used inline (extension/mobile) or in fixed position (web).
|
|
28
|
+
* Delegates click handling to the NotificationService.
|
|
29
|
+
*
|
|
30
|
+
* Features:
|
|
31
|
+
* - Maps notification API types to BannerTemplate props
|
|
32
|
+
* - Delegates click actions to NotificationService via onNotificationClick
|
|
33
|
+
* - Extracts background images and icons from notification content
|
|
34
|
+
*
|
|
35
|
+
* Notification API Type Mapping:
|
|
36
|
+
* - content.title → Banner title
|
|
37
|
+
* - content.subtitle → Banner description
|
|
38
|
+
* - content.background.link → Background image URL (when backgroundType is IMAGE)
|
|
39
|
+
* - content.background.backgroundOnClick → Handled by NotificationService
|
|
40
|
+
*/
|
|
41
|
+
export const InlineBannerNotification = memo(function InlineBannerNotification({
|
|
42
|
+
notification,
|
|
43
|
+
onNotificationClick,
|
|
44
|
+
width,
|
|
45
|
+
renderButton = false,
|
|
46
|
+
}: InlineBannerNotificationProps) {
|
|
47
|
+
const { t, i18n } = useTranslation()
|
|
48
|
+
const content = notification.content
|
|
49
|
+
|
|
50
|
+
const handleClose = (): void => {
|
|
51
|
+
onNotificationClick?.(notification.id, { type: 'dismiss' })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const hasBackgroundClick = !!content?.background?.backgroundOnClick
|
|
55
|
+
|
|
56
|
+
const handleBannerPress = (): void => {
|
|
57
|
+
onNotificationClick?.(notification.id, { type: 'background' })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const backgroundImageUrl = useMemo(() => {
|
|
61
|
+
const background = content?.background
|
|
62
|
+
if (background && background.backgroundType === BackgroundType.IMAGE && background.link) {
|
|
63
|
+
return background.link
|
|
64
|
+
}
|
|
65
|
+
return undefined
|
|
66
|
+
}, [content?.background])
|
|
67
|
+
|
|
68
|
+
const darkModeBackgroundImageUrl = useMemo(() => {
|
|
69
|
+
const background = content?.background
|
|
70
|
+
if (background && background.backgroundType === BackgroundType.IMAGE && background.darkModeLink) {
|
|
71
|
+
return background.darkModeLink
|
|
72
|
+
}
|
|
73
|
+
return undefined
|
|
74
|
+
}, [content?.background])
|
|
75
|
+
|
|
76
|
+
const customIcon = useMemo(() => {
|
|
77
|
+
const parsed = parseCustomIconLink(content?.iconLink)
|
|
78
|
+
if (!parsed.IconComponent) {
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
const Icon = parsed.IconComponent
|
|
82
|
+
return <Icon size={BANNER_ICON_SIZE} color={parsed.colorToken ?? '$neutral1'} />
|
|
83
|
+
}, [content?.iconLink])
|
|
84
|
+
|
|
85
|
+
const button = useMemo((): BannerTemplateButton | undefined => {
|
|
86
|
+
if (!renderButton) {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
91
|
+
const firstButton = content?.buttons?.[0]
|
|
92
|
+
if (!firstButton) {
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Translate known i18n keys, otherwise use raw text
|
|
97
|
+
const buttonText = i18n.exists(firstButton.text) ? t(firstButton.text) : firstButton.text
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
text: buttonText,
|
|
101
|
+
isPrimary: firstButton.isPrimary ?? false,
|
|
102
|
+
onPress: (): void => {
|
|
103
|
+
onNotificationClick?.(notification.id, { type: 'button', index: 0 })
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}, [renderButton, content?.buttons, notification.id, onNotificationClick, t, i18n])
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<BannerTemplate
|
|
110
|
+
backgroundImageUrl={backgroundImageUrl}
|
|
111
|
+
darkModeBackgroundImageUrl={darkModeBackgroundImageUrl}
|
|
112
|
+
icon={customIcon}
|
|
113
|
+
iconUrl={customIcon ? undefined : content?.iconLink}
|
|
114
|
+
darkModeIconUrl={customIcon ? undefined : content?.darkModeIconLink}
|
|
115
|
+
title={content?.title ?? ''}
|
|
116
|
+
subtitle={content?.subtitle ?? ''}
|
|
117
|
+
width={width}
|
|
118
|
+
button={button}
|
|
119
|
+
onClose={handleClose}
|
|
120
|
+
onPress={hasBackgroundClick ? handleBannerPress : undefined}
|
|
121
|
+
/>
|
|
122
|
+
)
|
|
123
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type InAppNotification } from '@l.x/api'
|
|
2
|
+
import { type NotificationRenderer } from '@l.x/notifications/src/notification-renderer/NotificationRenderer'
|
|
3
|
+
|
|
4
|
+
export function createNotificationRenderer(ctx: {
|
|
5
|
+
render: (notification: InAppNotification) => () => void
|
|
6
|
+
canRender: (notification: InAppNotification) => boolean
|
|
7
|
+
}): NotificationRenderer {
|
|
8
|
+
return {
|
|
9
|
+
render: (notification: InAppNotification): (() => void) => {
|
|
10
|
+
return ctx.render(notification)
|
|
11
|
+
},
|
|
12
|
+
canRender: (notification: InAppNotification): boolean => {
|
|
13
|
+
return ctx.canRender(notification)
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { GeneratedIcon } from 'ui/src/components/factories/createIcon'
|
|
2
|
+
import { AlertTriangle } from 'ui/src/components/icons/AlertTriangle'
|
|
3
|
+
import { Bell } from 'ui/src/components/icons/Bell'
|
|
4
|
+
import { Chart } from 'ui/src/components/icons/Chart'
|
|
5
|
+
import { CheckCircleFilled } from 'ui/src/components/icons/CheckCircleFilled'
|
|
6
|
+
import { Coin } from 'ui/src/components/icons/Coin'
|
|
7
|
+
import { CoinConvert } from 'ui/src/components/icons/CoinConvert'
|
|
8
|
+
import { EthMini } from 'ui/src/components/icons/EthMini'
|
|
9
|
+
import { Gas } from 'ui/src/components/icons/Gas'
|
|
10
|
+
import { Gift } from 'ui/src/components/icons/Gift'
|
|
11
|
+
import { Globe } from 'ui/src/components/icons/Globe'
|
|
12
|
+
import { InfoCircleFilled } from 'ui/src/components/icons/InfoCircleFilled'
|
|
13
|
+
import { Lightning } from 'ui/src/components/icons/Lightning'
|
|
14
|
+
import { Rocket } from 'ui/src/components/icons/Rocket'
|
|
15
|
+
import { SendAction } from 'ui/src/components/icons/SendAction'
|
|
16
|
+
import { ShieldCheck } from 'ui/src/components/icons/ShieldCheck'
|
|
17
|
+
import { Star } from 'ui/src/components/icons/Star'
|
|
18
|
+
import { Wallet } from 'ui/src/components/icons/Wallet'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Map of custom icon names to their corresponding icon components.
|
|
22
|
+
* Used for parsing notification icon links in the format "custom:<iconName>" or "custom:<iconName>-$<colorToken>".
|
|
23
|
+
*/
|
|
24
|
+
export const CUSTOM_ICON_MAP: Record<string, GeneratedIcon> = {
|
|
25
|
+
lightning: Lightning,
|
|
26
|
+
wallet: Wallet,
|
|
27
|
+
chart: Chart,
|
|
28
|
+
gas: Gas,
|
|
29
|
+
coin: Coin,
|
|
30
|
+
'coin-convert': CoinConvert,
|
|
31
|
+
ethereum: EthMini,
|
|
32
|
+
rocket: Rocket,
|
|
33
|
+
star: Star,
|
|
34
|
+
gift: Gift,
|
|
35
|
+
check: CheckCircleFilled,
|
|
36
|
+
info: InfoCircleFilled,
|
|
37
|
+
shield: ShieldCheck,
|
|
38
|
+
bell: Bell,
|
|
39
|
+
send: SendAction,
|
|
40
|
+
globe: Globe,
|
|
41
|
+
// Alert/warning icons
|
|
42
|
+
'alert-triangle': AlertTriangle,
|
|
43
|
+
'caution-triangle': AlertTriangle,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ParsedCustomIcon {
|
|
47
|
+
iconName: string
|
|
48
|
+
colorToken: string | undefined
|
|
49
|
+
IconComponent: GeneratedIcon | undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parses a custom icon link string and returns the icon component and color token.
|
|
54
|
+
*
|
|
55
|
+
* Supports two formats:
|
|
56
|
+
* - "custom:<iconName>" - Returns icon with no color token (use default)
|
|
57
|
+
* - "custom:<iconName>-$<colorToken>" - Returns icon with specified color token
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* parseCustomIconLink("custom:globe") // { iconName: "globe", colorToken: undefined, IconComponent: Globe }
|
|
61
|
+
* parseCustomIconLink("custom:lightning-$accent1") // { iconName: "lightning", colorToken: "$accent1", IconComponent: Lightning }
|
|
62
|
+
* parseCustomIconLink("https://example.com/icon.png") // { iconName: "", colorToken: undefined, IconComponent: undefined }
|
|
63
|
+
*/
|
|
64
|
+
export function parseCustomIconLink(iconLink: string | undefined): ParsedCustomIcon {
|
|
65
|
+
if (!iconLink || typeof iconLink !== 'string' || !iconLink.startsWith('custom:')) {
|
|
66
|
+
return { iconName: '', colorToken: undefined, IconComponent: undefined }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Remove "custom:" prefix
|
|
70
|
+
const customPart = iconLink.slice(7)
|
|
71
|
+
|
|
72
|
+
// Check for color token format: "iconName-$colorToken"
|
|
73
|
+
const colorTokenMatch = customPart.match(/^(.+)-(\$\w+)$/)
|
|
74
|
+
if (colorTokenMatch && colorTokenMatch[1] && colorTokenMatch[2]) {
|
|
75
|
+
const iconName = colorTokenMatch[1]
|
|
76
|
+
const colorToken = colorTokenMatch[2]
|
|
77
|
+
return {
|
|
78
|
+
iconName,
|
|
79
|
+
colorToken,
|
|
80
|
+
IconComponent: CUSTOM_ICON_MAP[iconName],
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Simple format: just "iconName"
|
|
85
|
+
const iconName = customPart.toLowerCase()
|
|
86
|
+
return {
|
|
87
|
+
iconName,
|
|
88
|
+
colorToken: undefined,
|
|
89
|
+
IconComponent: CUSTOM_ICON_MAP[iconName],
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Gets the icon component for a custom icon link, returning undefined if not found.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* getCustomIconComponent("custom:globe") // Globe
|
|
98
|
+
* getCustomIconComponent("custom:lightning-$accent1") // Lightning
|
|
99
|
+
* getCustomIconComponent("https://example.com/icon.png") // undefined
|
|
100
|
+
*/
|
|
101
|
+
export function getCustomIconComponent(iconLink: string | undefined): GeneratedIcon | undefined {
|
|
102
|
+
return parseCustomIconLink(iconLink).IconComponent
|
|
103
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type NotificationDataSource } from '@luxfi/notifications/src/notification-data-source/NotificationDataSource'
|
|
2
|
+
import { type NotificationProcessor } from '@luxfi/notifications/src/notification-processor/NotificationProcessor'
|
|
3
|
+
import { type NotificationRenderer } from '@luxfi/notifications/src/notification-renderer/NotificationRenderer'
|
|
4
|
+
import { type NotificationTelemetry } from '@luxfi/notifications/src/notification-telemetry/NotificationTelemetry'
|
|
5
|
+
import { type NotificationTracker } from '@luxfi/notifications/src/notification-tracker/NotificationTracker'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Represents what was clicked on a notification
|
|
9
|
+
*/
|
|
10
|
+
export type NotificationClickTarget = { type: 'button'; index: number } | { type: 'background' } | { type: 'dismiss' }
|
|
11
|
+
|
|
12
|
+
export interface NotificationServiceConfig {
|
|
13
|
+
// Multiple sources can feed notifications
|
|
14
|
+
dataSources: NotificationDataSource[]
|
|
15
|
+
tracker: NotificationTracker
|
|
16
|
+
processor: NotificationProcessor
|
|
17
|
+
renderer: NotificationRenderer
|
|
18
|
+
telemetry?: NotificationTelemetry
|
|
19
|
+
// Platform-specific handler for navigation/link clicks
|
|
20
|
+
// Should handle both internal navigation (same-origin) and external links
|
|
21
|
+
onNavigate?: (url: string) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface NotificationService {
|
|
25
|
+
// Initialize and start fetching notifications
|
|
26
|
+
initialize(): Promise<void>
|
|
27
|
+
/**
|
|
28
|
+
* Handle a render failure (e.g., unknown notification style)
|
|
29
|
+
* Cleans up the render without marking the notification as processed,
|
|
30
|
+
* allowing it to be re-rendered when correct data arrives
|
|
31
|
+
*/
|
|
32
|
+
onRenderFailed(notificationId: string): void
|
|
33
|
+
/**
|
|
34
|
+
* Handle a click on a notification (button, background, dismiss, or acknowledge)
|
|
35
|
+
* Executes all actions specified in the onClick array for the clicked target
|
|
36
|
+
* @param notificationId - ID of the notification that was clicked
|
|
37
|
+
* @param target - What was clicked (button, background, dismiss, or acknowledge)
|
|
38
|
+
*/
|
|
39
|
+
onNotificationClick(notificationId: string, target: NotificationClickTarget): void
|
|
40
|
+
/**
|
|
41
|
+
* Handle a notification being shown to the user
|
|
42
|
+
* @param notificationId - ID of the notification that was shown
|
|
43
|
+
*/
|
|
44
|
+
onNotificationShown(notificationId: string): void
|
|
45
|
+
// Trigger an immediate re-poll on all data sources that support it
|
|
46
|
+
refresh(): Promise<void>
|
|
47
|
+
// Cleanup and teardown
|
|
48
|
+
destroy(): void
|
|
49
|
+
}
|