@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.
Files changed (48) hide show
  1. package/.depcheckrc +14 -0
  2. package/.eslintrc.js +20 -0
  3. package/LICENSE +122 -0
  4. package/README.md +548 -0
  5. package/package.json +42 -1
  6. package/project.json +24 -0
  7. package/src/getIsNotificationServiceLocalOverrideEnabled.ts +7 -0
  8. package/src/global.d.ts +2 -0
  9. package/src/index.ts +41 -0
  10. package/src/notification-data-source/NotificationDataSource.ts +10 -0
  11. package/src/notification-data-source/getNotificationQueryOptions.ts +85 -0
  12. package/src/notification-data-source/implementations/createIntervalNotificationDataSource.ts +77 -0
  13. package/src/notification-data-source/implementations/createLocalTriggerDataSource.test.ts +492 -0
  14. package/src/notification-data-source/implementations/createLocalTriggerDataSource.ts +177 -0
  15. package/src/notification-data-source/implementations/createNotificationDataSource.ts +19 -0
  16. package/src/notification-data-source/implementations/createPollingNotificationDataSource.test.ts +398 -0
  17. package/src/notification-data-source/implementations/createPollingNotificationDataSource.ts +74 -0
  18. package/src/notification-data-source/implementations/createReactiveDataSource.ts +113 -0
  19. package/src/notification-data-source/types/ReactiveCondition.ts +60 -0
  20. package/src/notification-processor/NotificationProcessor.ts +26 -0
  21. package/src/notification-processor/implementations/createBaseNotificationProcessor.test.ts +854 -0
  22. package/src/notification-processor/implementations/createBaseNotificationProcessor.ts +254 -0
  23. package/src/notification-processor/implementations/createNotificationProcessor.test.ts +130 -0
  24. package/src/notification-processor/implementations/createNotificationProcessor.ts +15 -0
  25. package/src/notification-renderer/NotificationRenderer.ts +8 -0
  26. package/src/notification-renderer/components/BannerTemplate.tsx +188 -0
  27. package/src/notification-renderer/components/InlineBannerNotification.tsx +123 -0
  28. package/src/notification-renderer/implementations/createNotificationRenderer.ts +16 -0
  29. package/src/notification-renderer/utils/iconUtils.ts +103 -0
  30. package/src/notification-service/NotificationService.ts +49 -0
  31. package/src/notification-service/implementations/createNotificationService.test.ts +1092 -0
  32. package/src/notification-service/implementations/createNotificationService.ts +368 -0
  33. package/src/notification-telemetry/NotificationTelemetry.ts +44 -0
  34. package/src/notification-telemetry/implementations/createNotificationTelemetry.test.ts +99 -0
  35. package/src/notification-telemetry/implementations/createNotificationTelemetry.ts +33 -0
  36. package/src/notification-tracker/NotificationTracker.ts +14 -0
  37. package/src/notification-tracker/implementations/createApiNotificationTracker.test.ts +465 -0
  38. package/src/notification-tracker/implementations/createApiNotificationTracker.ts +154 -0
  39. package/src/notification-tracker/implementations/createNoopNotificationTracker.ts +44 -0
  40. package/src/notification-tracker/implementations/createNotificationTracker.ts +31 -0
  41. package/src/utils/formatNotificationType.test.ts +25 -0
  42. package/src/utils/formatNotificationType.ts +25 -0
  43. package/tsconfig.json +34 -0
  44. package/tsconfig.lint.json +8 -0
  45. package/vitest-setup.ts +1 -0
  46. package/vitest.config.ts +14 -0
  47. package/index.d.ts +0 -1
  48. 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
+ }