@luxexchange/notifications 1.0.2 → 1.0.3
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/README.md +548 -0
- package/package.json +5 -5
- package/project.json +1 -7
- package/src/notification-data-source/NotificationDataSource.ts +2 -0
- package/src/notification-data-source/getNotificationQueryOptions.ts +5 -5
- package/src/notification-data-source/implementations/createIntervalNotificationDataSource.ts +6 -2
- package/src/notification-data-source/implementations/createLocalTriggerDataSource.test.ts +1 -1
- package/src/notification-data-source/implementations/createLocalTriggerDataSource.ts +1 -1
- package/src/notification-data-source/implementations/createNotificationDataSource.ts +2 -2
- package/src/notification-data-source/implementations/createPollingNotificationDataSource.test.ts +2 -2
- package/src/notification-data-source/implementations/createPollingNotificationDataSource.ts +5 -5
- package/src/notification-data-source/implementations/createReactiveDataSource.ts +1 -1
- package/src/notification-processor/NotificationProcessor.ts +1 -1
- package/src/notification-processor/implementations/createBaseNotificationProcessor.test.ts +3 -3
- package/src/notification-processor/implementations/createBaseNotificationProcessor.ts +19 -4
- package/src/notification-processor/implementations/createNotificationProcessor.test.ts +2 -2
- package/src/notification-processor/implementations/createNotificationProcessor.ts +2 -2
- package/src/notification-renderer/NotificationRenderer.ts +1 -1
- package/src/notification-renderer/components/BannerTemplate.tsx +3 -3
- package/src/notification-renderer/components/InlineBannerNotification.tsx +1 -1
- package/src/notification-renderer/implementations/createNotificationRenderer.ts +2 -2
- package/src/notification-renderer/utils/iconUtils.ts +18 -18
- package/src/notification-service/NotificationService.ts +2 -0
- package/src/notification-service/implementations/createNotificationService.test.ts +7 -7
- package/src/notification-service/implementations/createNotificationService.ts +5 -1
- package/src/notification-telemetry/NotificationTelemetry.ts +1 -1
- package/src/notification-telemetry/implementations/createNotificationTelemetry.test.ts +2 -2
- package/src/notification-telemetry/implementations/createNotificationTelemetry.ts +2 -2
- package/src/notification-tracker/implementations/createApiNotificationTracker.test.ts +3 -3
- package/src/notification-tracker/implementations/createApiNotificationTracker.ts +7 -7
- package/src/notification-tracker/implementations/createNoopNotificationTracker.ts +3 -3
- package/src/notification-tracker/implementations/createNotificationTracker.ts +1 -1
- package/src/utils/formatNotificationType.test.ts +2 -2
- package/src/utils/formatNotificationType.ts +1 -1
- package/tsconfig.json +15 -5
- package/tsconfig.lint.json +8 -0
- package/vitest-setup.ts +1 -0
- package/vitest.config.ts +14 -0
package/.depcheckrc
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ignores: [
|
|
2
|
+
# Test coverage tool
|
|
3
|
+
"@vitest/coverage-v8",
|
|
4
|
+
|
|
5
|
+
# Standard ignores
|
|
6
|
+
"typescript",
|
|
7
|
+
"@typescript/native-preview",
|
|
8
|
+
"depcheck",
|
|
9
|
+
|
|
10
|
+
# Internal packages / workspaces - used in test files
|
|
11
|
+
"@universe/notifications",
|
|
12
|
+
"@universe/api",
|
|
13
|
+
"utilities",
|
|
14
|
+
]
|
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
extends: ['@luxamm/eslint-config/lib'],
|
|
3
|
+
parserOptions: {
|
|
4
|
+
tsconfigRootDir: __dirname,
|
|
5
|
+
},
|
|
6
|
+
overrides: [
|
|
7
|
+
{
|
|
8
|
+
files: ['*.ts', '*.tsx'],
|
|
9
|
+
rules: {
|
|
10
|
+
'no-relative-import-paths/no-relative-import-paths': [
|
|
11
|
+
'error',
|
|
12
|
+
{
|
|
13
|
+
allowSameFolder: false,
|
|
14
|
+
prefix: '@luxexchange/notifications',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
# @universe/notifications
|
|
2
|
+
|
|
3
|
+
A platform-agnostic notification service for fetching, processing, storing, and displaying in-app notifications across web, mobile, and extension platforms.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Architecture](#architecture)
|
|
8
|
+
- [Core Concepts](#core-concepts)
|
|
9
|
+
- [Notification Types](#notification-types-contentstyle)
|
|
10
|
+
- [Data Source Patterns](#data-source-patterns)
|
|
11
|
+
- [Notification Chains](#notification-chains)
|
|
12
|
+
- [Notification ID Conventions](#notification-id-conventions)
|
|
13
|
+
- [User Actions](#user-actions-onclickaction)
|
|
14
|
+
- [Getting Started](#getting-started)
|
|
15
|
+
- [Platform Integrations](#platform-integrations)
|
|
16
|
+
- [Web](#web-integration)
|
|
17
|
+
- [Mobile](#mobile-integration)
|
|
18
|
+
- [Extension](#extension-integration)
|
|
19
|
+
- [Common Patterns](#common-patterns)
|
|
20
|
+
- [Testing](#testing)
|
|
21
|
+
- [Troubleshooting](#troubleshooting)
|
|
22
|
+
- [API Reference](#api-reference)
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
28
|
+
│ NotificationService │
|
|
29
|
+
│ (Orchestrator) │
|
|
30
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
31
|
+
│ │
|
|
32
|
+
│ ┌──────────────────┐ ┌───────────────────┐ ┌──────────────┐ │
|
|
33
|
+
│ │ DataSources │────▶│ Processor │────▶│ Renderer │ │
|
|
34
|
+
│ │ │ │ │ │ │ │
|
|
35
|
+
│ │ • Polling (API) │ │ • Filters tracked │ │ • canRender │ │
|
|
36
|
+
│ │ • Reactive │ │ • Limits by style │ │ • render() │ │
|
|
37
|
+
│ │ • LocalTrigger │ │ • Separates chains│ │ • cleanup() │ │
|
|
38
|
+
│ │ • Interval │ │ │ │ │ │
|
|
39
|
+
│ └──────────────────┘ └───────────────────┘ └──────────────┘ │
|
|
40
|
+
│ │ │ │ │
|
|
41
|
+
│ │ ▼ │ │
|
|
42
|
+
│ │ ┌───────────────────┐ │ │
|
|
43
|
+
│ │ │ Tracker │◀───────────┘ │
|
|
44
|
+
│ │ │ │ │
|
|
45
|
+
│ │ │ • isProcessed() │ │
|
|
46
|
+
│ │ │ • track() │ │
|
|
47
|
+
│ │ │ • cleanup() │ │
|
|
48
|
+
│ │ └───────────────────┘ │
|
|
49
|
+
│ │ │ │
|
|
50
|
+
│ ▼ ▼ │
|
|
51
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
52
|
+
│ │ Telemetry │ │
|
|
53
|
+
│ │ onNotificationReceived → onNotificationShown → onInteracted │ │
|
|
54
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
55
|
+
│ │
|
|
56
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Component Responsibilities
|
|
60
|
+
|
|
61
|
+
| Component | Responsibility |
|
|
62
|
+
|-----------|----------------|
|
|
63
|
+
| **DataSource** | Feeds notifications to the service (API polling, state subscriptions, local triggers) |
|
|
64
|
+
| **Processor** | Filters tracked notifications, enforces style limits, separates primary vs chained |
|
|
65
|
+
| **Renderer** | Platform-specific UI rendering with canRender/render/cleanup lifecycle |
|
|
66
|
+
| **Tracker** | Tracks acknowledged notifications to prevent re-showing |
|
|
67
|
+
| **Telemetry** | Reports lifecycle events (received, shown, interacted) to analytics |
|
|
68
|
+
|
|
69
|
+
### Lifecycle
|
|
70
|
+
|
|
71
|
+
1. **Initialize** — Service starts all data sources
|
|
72
|
+
2. **Receive** — Data sources emit notifications to the service
|
|
73
|
+
3. **Process** — Processor filters and categorizes notifications
|
|
74
|
+
4. **Render** — Renderer displays primary notifications, stores chained for later
|
|
75
|
+
5. **Interact** — User clicks trigger actions (DISMISS, ACK, POPUP, EXTERNAL_LINK)
|
|
76
|
+
6. **Track** — ACK action marks notification as processed
|
|
77
|
+
7. **Destroy** — Service cleans up data sources and active renders
|
|
78
|
+
|
|
79
|
+
## Core Concepts
|
|
80
|
+
|
|
81
|
+
### Notification Types (ContentStyle)
|
|
82
|
+
|
|
83
|
+
| Style | Description | Max Concurrent | Typical Use |
|
|
84
|
+
|-------|-------------|----------------|-------------|
|
|
85
|
+
| `MODAL` | Full-screen overlays requiring user action | 1 | Feature announcements, onboarding flows |
|
|
86
|
+
| `SYSTEM_BANNER` | System alerts (typically at screen bottom) | 1 | Offline status, storage warnings |
|
|
87
|
+
| `LOWER_LEFT_BANNER` | Promotional banners stacked in lower-left | 3 | Chain promotions, feature highlights |
|
|
88
|
+
|
|
89
|
+
### Data Source Patterns
|
|
90
|
+
|
|
91
|
+
The service supports four patterns for feeding notifications:
|
|
92
|
+
|
|
93
|
+
#### 1. Polling (Backend API via React Query)
|
|
94
|
+
|
|
95
|
+
Best for: Backend-driven notifications with automatic caching and refetch.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { createPollingNotificationDataSource, getNotificationQueryOptions } from '@universe/notifications'
|
|
99
|
+
|
|
100
|
+
const pollingDataSource = createPollingNotificationDataSource({
|
|
101
|
+
queryClient,
|
|
102
|
+
queryOptions: getNotificationQueryOptions({ address, platformType, appId }),
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### 2. Reactive (Push-based Subscriptions)
|
|
107
|
+
|
|
108
|
+
Best for: Instant response to state changes (network status, storage quota).
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { createReactiveDataSource, type ReactiveCondition } from '@universe/notifications'
|
|
112
|
+
|
|
113
|
+
const offlineCondition: ReactiveCondition<{ isConnected: boolean }> = {
|
|
114
|
+
notificationId: 'local:session:offline',
|
|
115
|
+
subscribe: (onStateChange) => {
|
|
116
|
+
return NetInfo.addEventListener((state) => {
|
|
117
|
+
onStateChange({ isConnected: state.isConnected })
|
|
118
|
+
})
|
|
119
|
+
},
|
|
120
|
+
shouldShow: (state) => state.isConnected === false,
|
|
121
|
+
createNotification: () => new Notification({ /* ... */ }),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const reactiveDataSource = createReactiveDataSource({
|
|
125
|
+
condition: offlineCondition,
|
|
126
|
+
tracker,
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### 3. LocalTrigger (Condition-based Polling)
|
|
131
|
+
|
|
132
|
+
Best for: Periodic checks of local state (Redux selectors, app conditions).
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { createLocalTriggerDataSource, type TriggerCondition } from '@universe/notifications'
|
|
136
|
+
|
|
137
|
+
const backupTrigger: TriggerCondition = {
|
|
138
|
+
id: 'local:backup_reminder',
|
|
139
|
+
shouldShow: async () => {
|
|
140
|
+
const account = selectActiveAccount(getState())
|
|
141
|
+
return account && !hasExternalBackup(account)
|
|
142
|
+
},
|
|
143
|
+
createNotification: () => new Notification({ /* ... */ }),
|
|
144
|
+
onAcknowledge: () => dispatch(setLastSeenTs(Date.now())),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const localTriggerDataSource = createLocalTriggerDataSource({
|
|
148
|
+
triggers: [backupTrigger],
|
|
149
|
+
tracker,
|
|
150
|
+
pollIntervalMs: 5000,
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### 4. Interval (Simple Periodic Checks)
|
|
155
|
+
|
|
156
|
+
Best for: Custom periodic notification fetching.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { createIntervalNotificationDataSource } from '@universe/notifications'
|
|
160
|
+
|
|
161
|
+
const intervalDataSource = createIntervalNotificationDataSource({
|
|
162
|
+
pollIntervalMs: 30000,
|
|
163
|
+
source: 'legacy_banners',
|
|
164
|
+
logFileTag: 'LegacyBanners',
|
|
165
|
+
getNotifications: async () => fetchLegacyBanners(),
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Notification Chains
|
|
170
|
+
|
|
171
|
+
Notifications can trigger follow-up notifications using the `POPUP` action:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Step 1: User sees welcome banner
|
|
175
|
+
{
|
|
176
|
+
id: 'welcome_step_1',
|
|
177
|
+
content: {
|
|
178
|
+
buttons: [{
|
|
179
|
+
text: 'Learn More',
|
|
180
|
+
onClick: {
|
|
181
|
+
onClick: [OnClickAction.DISMISS, OnClickAction.POPUP],
|
|
182
|
+
onClickLink: 'welcome_step_2' // ← triggers next notification
|
|
183
|
+
}
|
|
184
|
+
}]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Step 2: Detailed modal (stored as "chained" until triggered)
|
|
189
|
+
{
|
|
190
|
+
id: 'welcome_step_2',
|
|
191
|
+
content: { style: ContentStyle.MODAL, /* ... */ }
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The processor automatically identifies root vs chained notifications using graph analysis.
|
|
196
|
+
|
|
197
|
+
### Notification ID Conventions
|
|
198
|
+
|
|
199
|
+
| Prefix | Storage | Behavior |
|
|
200
|
+
|--------|---------|----------|
|
|
201
|
+
| (none) | API + localStorage | Permanent tracking, synced with backend |
|
|
202
|
+
| `local:` | localStorage only | Permanent tracking, local only (no API calls) |
|
|
203
|
+
| `local:session:` | sessionStorage | Resets on app restart (e.g., offline banner) |
|
|
204
|
+
|
|
205
|
+
### User Actions (OnClickAction)
|
|
206
|
+
|
|
207
|
+
| Action | Effect |
|
|
208
|
+
|--------|--------|
|
|
209
|
+
| `DISMISS` | Hides the notification (can reappear if not ACK'd) |
|
|
210
|
+
| `ACK` | Marks as acknowledged, prevents re-showing |
|
|
211
|
+
| `POPUP` | Shows the notification specified in `onClickLink` |
|
|
212
|
+
| `EXTERNAL_LINK` | Navigates to URL in `onClickLink` via `onNavigate` handler |
|
|
213
|
+
|
|
214
|
+
Actions are combined in arrays and executed sequentially:
|
|
215
|
+
```typescript
|
|
216
|
+
onClick: [OnClickAction.DISMISS, OnClickAction.ACK] // Hide and mark processed
|
|
217
|
+
onClick: [OnClickAction.DISMISS, OnClickAction.POPUP] // Hide and show next
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Getting Started
|
|
221
|
+
|
|
222
|
+
### Initialize the Service
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import {
|
|
226
|
+
createNotificationService,
|
|
227
|
+
createPollingNotificationDataSource,
|
|
228
|
+
createBaseNotificationProcessor,
|
|
229
|
+
createNotificationTracker,
|
|
230
|
+
createNotificationRenderer,
|
|
231
|
+
createNotificationTelemetry,
|
|
232
|
+
} from '@universe/notifications'
|
|
233
|
+
|
|
234
|
+
const notificationService = createNotificationService({
|
|
235
|
+
dataSources: [
|
|
236
|
+
createPollingNotificationDataSource({ queryClient, queryOptions }),
|
|
237
|
+
// Add more data sources as needed
|
|
238
|
+
],
|
|
239
|
+
tracker: createNotificationTracker(storageAdapter),
|
|
240
|
+
processor: createBaseNotificationProcessor(tracker),
|
|
241
|
+
renderer: createNotificationRenderer({ onRender, canRender }),
|
|
242
|
+
telemetry: createNotificationTelemetry({ analytics }),
|
|
243
|
+
onNavigate: (url) => window.open(url, '_blank'),
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
await notificationService.initialize()
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Handle User Interactions
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// When user clicks a button (index 0)
|
|
253
|
+
notificationService.onNotificationClick(notificationId, { type: 'button', index: 0 })
|
|
254
|
+
|
|
255
|
+
// When user clicks the dismiss/close button
|
|
256
|
+
notificationService.onNotificationClick(notificationId, { type: 'dismiss' })
|
|
257
|
+
|
|
258
|
+
// When user clicks the background
|
|
259
|
+
notificationService.onNotificationClick(notificationId, { type: 'background' })
|
|
260
|
+
|
|
261
|
+
// When notification is shown to user (for telemetry)
|
|
262
|
+
notificationService.onNotificationShown(notificationId)
|
|
263
|
+
|
|
264
|
+
// When render fails (e.g., unknown notification style)
|
|
265
|
+
notificationService.onRenderFailed(notificationId)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Cleanup
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// On unmount or navigation
|
|
272
|
+
notificationService.destroy()
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Platform Integrations
|
|
276
|
+
|
|
277
|
+
### Web Integration
|
|
278
|
+
|
|
279
|
+
**Location:** `apps/web/src/notification-service/`
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
apps/web/src/notification-service/
|
|
283
|
+
├── WebNotificationService.tsx # Service initialization
|
|
284
|
+
├── createLocalStorageAdapter.ts # localStorage-based tracker
|
|
285
|
+
├── notification-renderer/
|
|
286
|
+
│ ├── NotificationContainer.tsx # Renders all notification types
|
|
287
|
+
│ ├── StackedLowerLeftBanners.tsx # Framer-motion stacking animations
|
|
288
|
+
│ ├── notificationStore.ts # Zustand store for UI state
|
|
289
|
+
│ └── components/
|
|
290
|
+
│ └── SystemBannerNotification.tsx
|
|
291
|
+
├── data-sources/
|
|
292
|
+
│ ├── createLegacyBannersNotificationDataSource.ts
|
|
293
|
+
│ └── createSystemAlertsDataSource.ts
|
|
294
|
+
└── telemetry/
|
|
295
|
+
└── getNotificationTelemetry.ts
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Key features:**
|
|
299
|
+
- Zustand store pattern for UI state management
|
|
300
|
+
- Framer-motion animations for stacked banners
|
|
301
|
+
- localStorage-based notification tracking
|
|
302
|
+
|
|
303
|
+
### Mobile Integration
|
|
304
|
+
|
|
305
|
+
**Location:** `apps/mobile/src/notification-service/`
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
apps/mobile/src/notification-service/
|
|
309
|
+
├── MobileNotificationServiceManager.tsx # Service manager component
|
|
310
|
+
├── MobileNotificationService.ts # Service initialization
|
|
311
|
+
├── createMobileStorageAdapter.ts # MMKV-based tracker
|
|
312
|
+
├── handleNotificationNavigation.ts # Navigation handler
|
|
313
|
+
├── notification-renderer/
|
|
314
|
+
│ ├── NotificationContainer.tsx # Routes to custom renderers
|
|
315
|
+
│ ├── SystemBannerPortal.tsx # Portal for system banners
|
|
316
|
+
│ └── createMobileNotificationRenderer.ts
|
|
317
|
+
├── renderers/
|
|
318
|
+
│ ├── BackupReminderModalRenderer.tsx # Custom modal for backup reminder
|
|
319
|
+
│ └── OfflineBannerRenderer.tsx # Custom banner for offline state
|
|
320
|
+
├── triggers/
|
|
321
|
+
│ ├── backupReminderTrigger.ts # LocalTrigger example
|
|
322
|
+
│ └── createMobileLocalTriggerDataSource.ts
|
|
323
|
+
└── data-sources/
|
|
324
|
+
├── reactive/
|
|
325
|
+
│ └── offlineCondition.ts # ReactiveCondition example
|
|
326
|
+
└── banners/ # Legacy banner data sources
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Key features:**
|
|
330
|
+
- MMKV storage for high-performance tracking
|
|
331
|
+
- Custom renderers for platform-specific UI (BackupReminder, OfflineBanner)
|
|
332
|
+
- NetInfo integration for offline detection via reactive data source
|
|
333
|
+
|
|
334
|
+
### Extension Integration
|
|
335
|
+
|
|
336
|
+
**Location:** `apps/extension/src/notification-service/`
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
apps/extension/src/notification-service/
|
|
340
|
+
├── ExtensionNotificationServiceManager.tsx # Service manager
|
|
341
|
+
├── ExtensionNotificationService.tsx # Service initialization
|
|
342
|
+
├── createChromeStorageAdapter.ts # Chrome Storage API tracker
|
|
343
|
+
├── notification-renderer/
|
|
344
|
+
│ ├── NotificationContainer.tsx
|
|
345
|
+
│ └── notificationStore.ts
|
|
346
|
+
├── renderers/
|
|
347
|
+
│ ├── AppRatingModalRenderer.tsx
|
|
348
|
+
│ └── StorageWarningModalRenderer.tsx
|
|
349
|
+
├── triggers/
|
|
350
|
+
│ ├── appRatingTrigger.ts
|
|
351
|
+
│ └── createExtensionLocalTriggerDataSource.ts
|
|
352
|
+
└── data-sources/
|
|
353
|
+
└── reactive/
|
|
354
|
+
└── storageWarningCondition.ts # Storage quota monitoring
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Key features:**
|
|
358
|
+
- Chrome Storage API for cross-session persistence
|
|
359
|
+
- Storage quota monitoring via reactive data source
|
|
360
|
+
- Special navigation handling (e.g., `unitag://` protocol)
|
|
361
|
+
|
|
362
|
+
## Common Patterns
|
|
363
|
+
|
|
364
|
+
### Creating a Local Trigger
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { type TriggerCondition } from '@universe/notifications'
|
|
368
|
+
|
|
369
|
+
export const REMINDER_NOTIFICATION_ID = 'local:my_reminder'
|
|
370
|
+
|
|
371
|
+
interface CreateReminderTriggerContext {
|
|
372
|
+
getState: () => AppState
|
|
373
|
+
dispatch: (action: AnyAction) => void
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function createReminderTrigger(ctx: CreateReminderTriggerContext): TriggerCondition {
|
|
377
|
+
const { getState, dispatch } = ctx
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
id: REMINDER_NOTIFICATION_ID,
|
|
381
|
+
|
|
382
|
+
shouldShow: async () => {
|
|
383
|
+
const state = getState()
|
|
384
|
+
const lastSeen = selectReminderLastSeen(state)
|
|
385
|
+
return Date.now() - lastSeen > ONE_DAY_MS
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
createNotification: () => new Notification({
|
|
389
|
+
id: REMINDER_NOTIFICATION_ID,
|
|
390
|
+
content: new Content({
|
|
391
|
+
style: ContentStyle.MODAL,
|
|
392
|
+
title: 'Reminder',
|
|
393
|
+
onDismissClick: new OnClick({
|
|
394
|
+
onClick: [OnClickAction.DISMISS, OnClickAction.ACK],
|
|
395
|
+
}),
|
|
396
|
+
}),
|
|
397
|
+
}),
|
|
398
|
+
|
|
399
|
+
onAcknowledge: () => {
|
|
400
|
+
dispatch(setReminderLastSeen(Date.now()))
|
|
401
|
+
},
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Creating a Reactive Condition
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { type ReactiveCondition } from '@universe/notifications'
|
|
410
|
+
|
|
411
|
+
export const STATUS_NOTIFICATION_ID = 'local:session:status'
|
|
412
|
+
|
|
413
|
+
interface StatusState {
|
|
414
|
+
isActive: boolean
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function createStatusCondition(ctx: { getState: () => AppState }): ReactiveCondition<StatusState> {
|
|
418
|
+
return {
|
|
419
|
+
notificationId: STATUS_NOTIFICATION_ID,
|
|
420
|
+
|
|
421
|
+
subscribe: (onStateChange) => {
|
|
422
|
+
// Subscribe to external state changes
|
|
423
|
+
const unsubscribe = someService.addEventListener((event) => {
|
|
424
|
+
onStateChange({ isActive: event.isActive })
|
|
425
|
+
})
|
|
426
|
+
return unsubscribe
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
shouldShow: (state) => state.isActive === false,
|
|
430
|
+
|
|
431
|
+
createNotification: (state) => new Notification({
|
|
432
|
+
id: STATUS_NOTIFICATION_ID,
|
|
433
|
+
content: new Content({
|
|
434
|
+
style: ContentStyle.SYSTEM_BANNER,
|
|
435
|
+
title: 'Service unavailable',
|
|
436
|
+
onDismissClick: new OnClick({
|
|
437
|
+
onClick: [OnClickAction.DISMISS, OnClickAction.ACK],
|
|
438
|
+
}),
|
|
439
|
+
}),
|
|
440
|
+
}),
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Custom Renderer with Notification Routing
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
import { createNotificationRenderer } from '@universe/notifications'
|
|
449
|
+
|
|
450
|
+
function isMyCustomNotification(notification: InAppNotification): boolean {
|
|
451
|
+
return notification.id === 'local:my_custom'
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const renderer = createNotificationRenderer({
|
|
455
|
+
canRender: (notification) => {
|
|
456
|
+
// Only one modal at a time
|
|
457
|
+
if (notification.content?.style === ContentStyle.MODAL) {
|
|
458
|
+
return !hasActiveModal()
|
|
459
|
+
}
|
|
460
|
+
return true
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
render: (notification) => {
|
|
464
|
+
// Route to appropriate renderer
|
|
465
|
+
if (isMyCustomNotification(notification)) {
|
|
466
|
+
return renderMyCustomModal(notification)
|
|
467
|
+
}
|
|
468
|
+
return renderDefaultNotification(notification)
|
|
469
|
+
},
|
|
470
|
+
})
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## Testing
|
|
474
|
+
|
|
475
|
+
### Mock Data Source
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
function createMockDataSource(): NotificationDataSource & {
|
|
479
|
+
emit: (notifications: InAppNotification[]) => void
|
|
480
|
+
} {
|
|
481
|
+
let callback: ((notifications: InAppNotification[], source: string) => void) | null = null
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
start: (onNotifications) => { callback = onNotifications },
|
|
485
|
+
stop: async () => { callback = null },
|
|
486
|
+
emit: (notifications) => callback?.(notifications, 'mock'),
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// In tests
|
|
491
|
+
const mockDataSource = createMockDataSource()
|
|
492
|
+
const service = createNotificationService({
|
|
493
|
+
dataSources: [mockDataSource],
|
|
494
|
+
// ...
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
await service.initialize()
|
|
498
|
+
mockDataSource.emit([testNotification])
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Mock Tracker
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
function createMockTracker(): NotificationTracker {
|
|
505
|
+
const processed = new Set<string>()
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
isProcessed: async (id) => processed.has(id),
|
|
509
|
+
getProcessedIds: async () => processed,
|
|
510
|
+
track: async (id) => { processed.add(id) },
|
|
511
|
+
cleanup: async () => { processed.clear() },
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Mock Renderer
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
function createMockRenderer(): NotificationRenderer & {
|
|
520
|
+
rendered: InAppNotification[]
|
|
521
|
+
} {
|
|
522
|
+
const rendered: InAppNotification[] = []
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
rendered,
|
|
526
|
+
canRender: () => true,
|
|
527
|
+
render: (notification) => {
|
|
528
|
+
rendered.push(notification)
|
|
529
|
+
return () => {
|
|
530
|
+
const index = rendered.indexOf(notification)
|
|
531
|
+
if (index > -1) rendered.splice(index, 1)
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Troubleshooting
|
|
539
|
+
|
|
540
|
+
| Issue | Cause | Solution |
|
|
541
|
+
|-------|-------|----------|
|
|
542
|
+
| Notification re-appears after dismiss | Using `DISMISS` without `ACK` | Add `ACK` to the onClick array |
|
|
543
|
+
| Notification never shows | Already tracked as processed | Clear storage or use a new notification ID |
|
|
544
|
+
| Chained notification not showing | Target not in chained map | Ensure API returns all chain members in same response |
|
|
545
|
+
| Multiple modals appearing | `canRender` not checking active modals | Implement modal count check in renderer |
|
|
546
|
+
| Session notification persists | Using `local:` instead of `local:session:` | Use `local:session:` prefix for session-scoped notifications |
|
|
547
|
+
| Reactive condition not updating | Not calling `onStateChange` | Ensure subscribe callback is invoked on state changes |
|
|
548
|
+
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luxexchange/notifications",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@bufbuild/protobuf": "1.10.0",
|
|
6
6
|
"@tanstack/react-query": "5.90.20",
|
|
7
|
-
"@
|
|
7
|
+
"@luxamm/client-notification-service": "0.0.114",
|
|
8
|
+
"@luxexchange/api": "workspace:*",
|
|
8
9
|
"ms": "2.1.3",
|
|
9
10
|
"react": "19.0.3",
|
|
10
11
|
"react-i18next": "14.1.0",
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
15
|
"@types/node": "22.13.1",
|
|
15
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
16
|
+
"@typescript/native-preview": "7.0.0-dev.20260311.1",
|
|
16
17
|
"@luxfi/eslint-config": "workspace:^",
|
|
17
18
|
"@vitest/coverage-v8": "3.2.1",
|
|
18
19
|
"depcheck": "1.4.7",
|
|
@@ -30,8 +31,7 @@
|
|
|
30
31
|
"lint:fix": "nx lint:fix notifications",
|
|
31
32
|
"typecheck": "nx typecheck notifications",
|
|
32
33
|
"typecheck:tsgo": "nx typecheck:tsgo notifications",
|
|
33
|
-
"test": "nx test notifications"
|
|
34
|
-
"test:coverage": "nx test:coverage notifications"
|
|
34
|
+
"test": "nx test notifications"
|
|
35
35
|
},
|
|
36
36
|
"nx": {
|
|
37
37
|
"includedScripts": []
|
package/project.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@
|
|
2
|
+
"name": "@luxexchange/notifications",
|
|
3
3
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
4
|
"sourceRoot": "pkgs/notifications/src",
|
|
5
5
|
"projectType": "library",
|
|
@@ -19,12 +19,6 @@
|
|
|
19
19
|
"options": {
|
|
20
20
|
"cwd": "{projectRoot}"
|
|
21
21
|
}
|
|
22
|
-
},
|
|
23
|
-
"test:coverage": {
|
|
24
|
-
"command": "vitest run --coverage",
|
|
25
|
-
"options": {
|
|
26
|
-
"cwd": "{projectRoot}"
|
|
27
|
-
}
|
|
28
22
|
}
|
|
29
23
|
}
|
|
30
24
|
}
|
|
@@ -5,4 +5,6 @@ export interface NotificationDataSource {
|
|
|
5
5
|
start(onNotifications: (notifications: InAppNotification[], source: string) => void): void
|
|
6
6
|
// Stop receiving notifications and cleanup
|
|
7
7
|
stop(): Promise<void>
|
|
8
|
+
// Trigger an immediate poll outside of the normal interval (optional)
|
|
9
|
+
refresh?(): Promise<void>
|
|
8
10
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { toPlainMessage } from '@bufbuild/protobuf'
|
|
2
2
|
import { queryOptions } from '@tanstack/react-query'
|
|
3
|
-
import { PlatformType } from '@
|
|
3
|
+
import { PlatformType } from '@luxamm/client-notification-service/dist/uniswap/notificationservice/v1/api_pb'
|
|
4
4
|
import type { InAppNotification, NotificationsApiClient } from '@luxexchange/api'
|
|
5
|
-
import { getLogger } from '
|
|
6
|
-
import { ReactQueryCacheKey } from '
|
|
7
|
-
import { type QueryOptionsResult } from '
|
|
8
|
-
import { ONE_MINUTE_MS } from '
|
|
5
|
+
import { getLogger } from 'utilities/src/logger/logger'
|
|
6
|
+
import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache'
|
|
7
|
+
import { type QueryOptionsResult } from 'utilities/src/reactQuery/queryOptions'
|
|
8
|
+
import { ONE_MINUTE_MS } from 'utilities/src/time/time'
|
|
9
9
|
|
|
10
10
|
const DEFAULT_POLL_INTERVAL_MS = 2 * ONE_MINUTE_MS
|
|
11
11
|
|
package/src/notification-data-source/implementations/createIntervalNotificationDataSource.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type InAppNotification } from '@luxexchange/api'
|
|
2
2
|
import { createNotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/implementations/createNotificationDataSource'
|
|
3
3
|
import { type NotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/NotificationDataSource'
|
|
4
|
-
import { getLogger } from '
|
|
4
|
+
import { getLogger } from 'utilities/src/logger/logger'
|
|
5
5
|
|
|
6
6
|
interface CreateIntervalNotificationDataSourceContext {
|
|
7
7
|
pollIntervalMs: number
|
|
@@ -69,5 +69,9 @@ export function createIntervalNotificationDataSource(
|
|
|
69
69
|
currentCallback = null
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
const refresh = async (): Promise<void> => {
|
|
73
|
+
await pollAndEmit('refresh')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { ...createNotificationDataSource({ start, stop }), refresh }
|
|
73
77
|
}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
Content,
|
|
3
3
|
Metadata,
|
|
4
4
|
Notification,
|
|
5
|
-
} from '@
|
|
5
|
+
} from '@luxamm/client-notification-service/dist/uniswap/notificationservice/v1/api_pb'
|
|
6
6
|
import type { InAppNotification } from '@luxexchange/api'
|
|
7
7
|
import { ContentStyle } from '@luxexchange/api'
|
|
8
8
|
import {
|
|
@@ -2,7 +2,7 @@ import { type InAppNotification } from '@luxexchange/api'
|
|
|
2
2
|
import { createNotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/implementations/createNotificationDataSource'
|
|
3
3
|
import { type NotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/NotificationDataSource'
|
|
4
4
|
import { type NotificationTracker } from '@luxexchange/notifications/src/notification-tracker/NotificationTracker'
|
|
5
|
-
import { getLogger } from '
|
|
5
|
+
import { getLogger } from 'utilities/src/logger/logger'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Configuration for a single trigger condition.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type InAppNotification } from '@
|
|
2
|
-
import { type NotificationDataSource } from '@
|
|
1
|
+
import { type InAppNotification } from '@luxexchange/api'
|
|
2
|
+
import { type NotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/NotificationDataSource'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Basic implementation of the NotificationDataSource interface.
|
package/src/notification-data-source/implementations/createPollingNotificationDataSource.test.ts
CHANGED
|
@@ -5,12 +5,12 @@ import {
|
|
|
5
5
|
Metadata,
|
|
6
6
|
Notification,
|
|
7
7
|
PlatformType,
|
|
8
|
-
} from '@
|
|
8
|
+
} from '@luxamm/client-notification-service/dist/uniswap/notificationservice/v1/api_pb'
|
|
9
9
|
import type { InAppNotification, NotificationsApiClient } from '@luxexchange/api'
|
|
10
10
|
import { ContentStyle } from '@luxexchange/api'
|
|
11
11
|
import { getNotificationQueryOptions } from '@luxexchange/notifications/src/notification-data-source/getNotificationQueryOptions'
|
|
12
12
|
import { createPollingNotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/implementations/createPollingNotificationDataSource'
|
|
13
|
-
import { ReactQueryCacheKey } from '
|
|
13
|
+
import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache'
|
|
14
14
|
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
|
15
15
|
|
|
16
16
|
function createMockNotification(id: string): InAppNotification {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type QueryClient, type QueryKey, QueryObserver } from '@tanstack/react-query'
|
|
2
|
-
import { type InAppNotification } from '@
|
|
3
|
-
import { createNotificationDataSource } from '@
|
|
4
|
-
import { type NotificationDataSource } from '@
|
|
5
|
-
import { getLogger } from '
|
|
6
|
-
import { type QueryOptionsResult } from '
|
|
2
|
+
import { type InAppNotification } from '@luxexchange/api'
|
|
3
|
+
import { createNotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/implementations/createNotificationDataSource'
|
|
4
|
+
import { type NotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/NotificationDataSource'
|
|
5
|
+
import { getLogger } from 'utilities/src/logger/logger'
|
|
6
|
+
import { type QueryOptionsResult } from 'utilities/src/reactQuery/queryOptions'
|
|
7
7
|
|
|
8
8
|
interface CreatePollingNotificationDataSourceContext<TQueryKey extends QueryKey = QueryKey> {
|
|
9
9
|
queryClient: QueryClient
|
|
@@ -3,7 +3,7 @@ import { createNotificationDataSource } from '@luxexchange/notifications/src/not
|
|
|
3
3
|
import { type NotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/NotificationDataSource'
|
|
4
4
|
import { type ReactiveCondition } from '@luxexchange/notifications/src/notification-data-source/types/ReactiveCondition'
|
|
5
5
|
import { type NotificationTracker } from '@luxexchange/notifications/src/notification-tracker/NotificationTracker'
|
|
6
|
-
import { getLogger } from '
|
|
6
|
+
import { getLogger } from 'utilities/src/logger/logger'
|
|
7
7
|
|
|
8
8
|
export interface CreateReactiveDataSourceContext<TState> {
|
|
9
9
|
/** The reactive condition that determines when to show the notification */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ContentStyle, type InAppNotification, OnClickAction } from '@
|
|
2
|
-
import { createBaseNotificationProcessor } from '@
|
|
3
|
-
import type { NotificationTracker } from '@
|
|
1
|
+
import { ContentStyle, type InAppNotification, OnClickAction } from '@luxexchange/api'
|
|
2
|
+
import { createBaseNotificationProcessor } from '@luxexchange/notifications/src/notification-processor/implementations/createBaseNotificationProcessor'
|
|
3
|
+
import type { NotificationTracker } from '@luxexchange/notifications/src/notification-tracker/NotificationTracker'
|
|
4
4
|
import { describe, expect, it } from 'vitest'
|
|
5
5
|
|
|
6
6
|
describe('createBaseNotificationProcessor', () => {
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type NotificationProcessorResult,
|
|
6
6
|
} from '@luxexchange/notifications/src/notification-processor/NotificationProcessor'
|
|
7
7
|
import { type NotificationTracker } from '@luxexchange/notifications/src/notification-tracker/NotificationTracker'
|
|
8
|
-
import { getLogger } from '
|
|
8
|
+
import { getLogger } from 'utilities/src/logger/logger'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Creates a base notification processor that implements style-based deduplication and limiting,
|
|
@@ -43,12 +43,14 @@ export function createBaseNotificationProcessor(tracker: NotificationTracker): N
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Step 3: Filter out notifications that are locally tracked or don't have DISMISS action
|
|
46
|
+
// Step 3: Filter out notifications that are locally tracked or don't have DISMISS action.
|
|
47
|
+
// Required cards are exempt from the DISMISS check — they self-dismiss when their
|
|
48
|
+
// underlying data condition resolves (e.g. wallet receives funds).
|
|
47
49
|
const filteredPrimary = primaryNotifications.filter((notification) => {
|
|
48
50
|
if (processedIds.has(notification.id)) {
|
|
49
51
|
return false
|
|
50
52
|
}
|
|
51
|
-
if (!hasDismissAction(notification)) {
|
|
53
|
+
if (!isRequiredCard(notification) && !hasDismissAction(notification)) {
|
|
52
54
|
getLogger().warn(
|
|
53
55
|
'createBaseNotificationProcessor',
|
|
54
56
|
'process',
|
|
@@ -64,7 +66,7 @@ export function createBaseNotificationProcessor(tracker: NotificationTracker): N
|
|
|
64
66
|
if (processedIds.has(notification.id)) {
|
|
65
67
|
return false
|
|
66
68
|
}
|
|
67
|
-
if (!hasDismissAction(notification)) {
|
|
69
|
+
if (!isRequiredCard(notification) && !hasDismissAction(notification)) {
|
|
68
70
|
getLogger().warn(
|
|
69
71
|
'createBaseNotificationProcessor',
|
|
70
72
|
'process',
|
|
@@ -93,6 +95,19 @@ export function createBaseNotificationProcessor(tracker: NotificationTracker): N
|
|
|
93
95
|
})
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Returns true if the notification is a required card — one that self-dismisses when its
|
|
100
|
+
* underlying data condition resolves rather than via explicit user action.
|
|
101
|
+
*/
|
|
102
|
+
function isRequiredCard(notification: InAppNotification): boolean {
|
|
103
|
+
try {
|
|
104
|
+
const extra = notification.content?.extra ? JSON.parse(notification.content.extra) : {}
|
|
105
|
+
return extra.cardType === 'required'
|
|
106
|
+
} catch {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
/**
|
|
97
112
|
* Checks if a notification has at least one DISMISS action in any of its click configurations.
|
|
98
113
|
* Every notification must have a way to be dismissed.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { InAppNotification } from '@
|
|
2
|
-
import { createNotificationProcessor } from '@
|
|
1
|
+
import type { InAppNotification } from '@luxexchange/api'
|
|
2
|
+
import { createNotificationProcessor } from '@luxexchange/notifications/src/notification-processor/implementations/createNotificationProcessor'
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
5
|
describe('createNotificationProcessor', () => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type InAppNotification } from '@
|
|
1
|
+
import { type InAppNotification } from '@luxexchange/api'
|
|
2
2
|
import {
|
|
3
3
|
type NotificationProcessor,
|
|
4
4
|
type NotificationProcessorResult,
|
|
5
|
-
} from '@
|
|
5
|
+
} from '@luxexchange/notifications/src/notification-processor/NotificationProcessor'
|
|
6
6
|
|
|
7
7
|
export function createNotificationProcessor(ctx: {
|
|
8
8
|
process: (notifications: InAppNotification[]) => Promise<NotificationProcessorResult>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ReactNode } from 'react'
|
|
2
|
-
import { Button, Flex, IconButton, styled, Text, useIsDarkMode } from '
|
|
3
|
-
import { X } from '
|
|
4
|
-
import { zIndexes } from '
|
|
2
|
+
import { Button, Flex, IconButton, styled, Text, useIsDarkMode } from 'ui/src'
|
|
3
|
+
import { X } from 'ui/src/components/icons/X'
|
|
4
|
+
import { zIndexes } from 'ui/src/theme'
|
|
5
5
|
|
|
6
6
|
const BANNER_WIDTH = 260
|
|
7
7
|
const BANNER_HEIGHT = 150
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BackgroundType } from '@
|
|
1
|
+
import { BackgroundType } from '@luxamm/client-notification-service/dist/uniswap/notificationservice/v1/api_pb'
|
|
2
2
|
import type { InAppNotification } from '@luxexchange/api'
|
|
3
3
|
import {
|
|
4
4
|
BannerTemplate,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type InAppNotification } from '@
|
|
2
|
-
import { type NotificationRenderer } from '@
|
|
1
|
+
import { type InAppNotification } from '@luxexchange/api'
|
|
2
|
+
import { type NotificationRenderer } from '@luxexchange/notifications/src/notification-renderer/NotificationRenderer'
|
|
3
3
|
|
|
4
4
|
export function createNotificationRenderer(ctx: {
|
|
5
5
|
render: (notification: InAppNotification) => () => void
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import type { GeneratedIcon } from '
|
|
2
|
-
import { AlertTriangle } from '
|
|
3
|
-
import { Bell } from '
|
|
4
|
-
import { Chart } from '
|
|
5
|
-
import { CheckCircleFilled } from '
|
|
6
|
-
import { Coin } from '
|
|
7
|
-
import { CoinConvert } from '
|
|
8
|
-
import { EthMini } from '
|
|
9
|
-
import { Gas } from '
|
|
10
|
-
import { Gift } from '
|
|
11
|
-
import { Globe } from '
|
|
12
|
-
import { InfoCircleFilled } from '
|
|
13
|
-
import { Lightning } from '
|
|
14
|
-
import { Rocket } from '
|
|
15
|
-
import { SendAction } from '
|
|
16
|
-
import { ShieldCheck } from '
|
|
17
|
-
import { Star } from '
|
|
18
|
-
import { Wallet } from '
|
|
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
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Map of custom icon names to their corresponding icon components.
|
|
@@ -42,6 +42,8 @@ export interface NotificationService {
|
|
|
42
42
|
* @param notificationId - ID of the notification that was shown
|
|
43
43
|
*/
|
|
44
44
|
onNotificationShown(notificationId: string): void
|
|
45
|
+
// Trigger an immediate re-poll on all data sources that support it
|
|
46
|
+
refresh(): Promise<void>
|
|
45
47
|
// Cleanup and teardown
|
|
46
48
|
destroy(): void
|
|
47
49
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { type InAppNotification, OnClickAction } from '@
|
|
2
|
-
import type { NotificationDataSource } from '@
|
|
3
|
-
import type { NotificationProcessor } from '@
|
|
4
|
-
import type { NotificationRenderer } from '@
|
|
5
|
-
import { createNotificationService } from '@
|
|
1
|
+
import { type InAppNotification, OnClickAction } from '@luxexchange/api'
|
|
2
|
+
import type { NotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/NotificationDataSource'
|
|
3
|
+
import type { NotificationProcessor } from '@luxexchange/notifications/src/notification-processor/NotificationProcessor'
|
|
4
|
+
import type { NotificationRenderer } from '@luxexchange/notifications/src/notification-renderer/NotificationRenderer'
|
|
5
|
+
import { createNotificationService } from '@luxexchange/notifications/src/notification-service/implementations/createNotificationService'
|
|
6
6
|
import type {
|
|
7
7
|
NotificationTracker,
|
|
8
8
|
TrackingMetadata,
|
|
9
|
-
} from '@
|
|
10
|
-
import { sleep } from '
|
|
9
|
+
} from '@luxexchange/notifications/src/notification-tracker/NotificationTracker'
|
|
10
|
+
import { sleep } from 'utilities/src/time/timing'
|
|
11
11
|
import { describe, expect, it, vi } from 'vitest'
|
|
12
12
|
|
|
13
13
|
describe('createNotificationService', () => {
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type NotificationServiceConfig,
|
|
6
6
|
} from '@luxfi/notifications/src/notification-service/NotificationService'
|
|
7
7
|
import ms from 'ms'
|
|
8
|
-
import { getLogger } from '
|
|
8
|
+
import { getLogger } from 'utilities/src/logger/logger'
|
|
9
9
|
|
|
10
10
|
// Module-level singletons to track notification telemetry across service recreations.
|
|
11
11
|
// This prevents duplicate telemetry events when the service is destroyed
|
|
@@ -329,6 +329,10 @@ export function createNotificationService(config: NotificationServiceConfig): No
|
|
|
329
329
|
})
|
|
330
330
|
},
|
|
331
331
|
|
|
332
|
+
async refresh(): Promise<void> {
|
|
333
|
+
await Promise.all(dataSources.map((dataSource) => dataSource.refresh?.()))
|
|
334
|
+
},
|
|
335
|
+
|
|
332
336
|
destroy(): void {
|
|
333
337
|
// Clean up old tracked notifications on teardown
|
|
334
338
|
const cleanupThreshold = Date.now() - CLEANUP_OLDER_THAN_MS
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ContentStyle } from '@
|
|
2
|
-
import { createNotificationTelemetry } from '@
|
|
1
|
+
import { ContentStyle } from '@luxexchange/api'
|
|
2
|
+
import { createNotificationTelemetry } from '@luxexchange/notifications/src/notification-telemetry/implementations/createNotificationTelemetry'
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
5
|
describe('createNotificationTelemetry', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type NotificationTelemetry } from '@
|
|
2
|
-
import { formatNotificationType } from '@
|
|
1
|
+
import { type NotificationTelemetry } from '@luxexchange/notifications/src/notification-telemetry/NotificationTelemetry'
|
|
2
|
+
import { formatNotificationType } from '@luxexchange/notifications/src/utils/formatNotificationType'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Basic implementation of the NotificationTelemetry interface.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { QueryClient } from '@tanstack/react-query'
|
|
2
|
-
import type { NotificationsApiClient } from '@
|
|
3
|
-
import { createApiNotificationTracker } from '@
|
|
4
|
-
import type { TrackingMetadata } from '@
|
|
2
|
+
import type { NotificationsApiClient } from '@luxexchange/api'
|
|
3
|
+
import { createApiNotificationTracker } from '@luxexchange/notifications/src/notification-tracker/implementations/createApiNotificationTracker'
|
|
4
|
+
import type { TrackingMetadata } from '@luxexchange/notifications/src/notification-tracker/NotificationTracker'
|
|
5
5
|
import { describe, expect, it, type Mock, vi } from 'vitest'
|
|
6
6
|
|
|
7
7
|
describe('createApiNotificationTracker', () => {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { FetchQueryOptions, QueryClient } from '@tanstack/react-query'
|
|
2
|
-
import type { NotificationsApiClient } from '@
|
|
3
|
-
import { createNotificationTracker } from '@
|
|
2
|
+
import type { NotificationsApiClient } from '@luxexchange/api'
|
|
3
|
+
import { createNotificationTracker } from '@luxexchange/notifications/src/notification-tracker/implementations/createNotificationTracker'
|
|
4
4
|
import {
|
|
5
5
|
NotificationTracker,
|
|
6
6
|
TrackingMetadata,
|
|
7
|
-
} from '@
|
|
8
|
-
import { getLogger } from '
|
|
9
|
-
import { ReactQueryCacheKey } from '
|
|
7
|
+
} from '@luxexchange/notifications/src/notification-tracker/NotificationTracker'
|
|
8
|
+
import { getLogger } from 'utilities/src/logger/logger'
|
|
9
|
+
import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Context for creating an API-based notification tracker
|
|
@@ -34,8 +34,8 @@ export interface ApiNotificationTrackerContext {
|
|
|
34
34
|
*
|
|
35
35
|
* Example usage:
|
|
36
36
|
* ```typescript
|
|
37
|
-
* import { createApiNotificationTracker } from '@
|
|
38
|
-
* import { createNotificationsApiClient } from '@
|
|
37
|
+
* import { createApiNotificationTracker } from '@luxexchange/notifications'
|
|
38
|
+
* import { createNotificationsApiClient } from '@luxexchange/api'
|
|
39
39
|
*
|
|
40
40
|
* const apiClient = createNotificationsApiClient({
|
|
41
41
|
* fetchClient: myFetchClient,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { createNotificationTracker } from '@
|
|
1
|
+
import { createNotificationTracker } from '@luxexchange/notifications/src/notification-tracker/implementations/createNotificationTracker'
|
|
2
2
|
import {
|
|
3
3
|
NotificationTracker,
|
|
4
4
|
TrackingMetadata,
|
|
5
|
-
} from '@
|
|
5
|
+
} from '@luxexchange/notifications/src/notification-tracker/NotificationTracker'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Creates a no-op notification tracker that doesn't persist any state.
|
|
9
9
|
*
|
|
10
10
|
* Example usage:
|
|
11
11
|
* ```typescript
|
|
12
|
-
* import { createNoopNotificationTracker } from '@
|
|
12
|
+
* import { createNoopNotificationTracker } from '@luxexchange/notifications'
|
|
13
13
|
*
|
|
14
14
|
* const tracker = createNoopNotificationTracker()
|
|
15
15
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NotificationTracker,
|
|
3
3
|
TrackingMetadata,
|
|
4
|
-
} from '@
|
|
4
|
+
} from '@luxexchange/notifications/src/notification-tracker/NotificationTracker'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Basic implementation of the NotificationTracker interface.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ContentStyle } from '@
|
|
2
|
-
import { formatNotificationType } from '@
|
|
1
|
+
import { ContentStyle } from '@luxexchange/api'
|
|
2
|
+
import { formatNotificationType } from '@luxexchange/notifications/src/utils/formatNotificationType'
|
|
3
3
|
import { describe, expect, it } from 'vitest'
|
|
4
4
|
|
|
5
5
|
describe('formatNotificationType', () => {
|
package/tsconfig.json
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "../../config/tsconfig/app.json",
|
|
3
|
-
"include": [
|
|
4
|
-
|
|
3
|
+
"include": [
|
|
4
|
+
"src/**/*.ts",
|
|
5
|
+
"src/**/*.tsx",
|
|
6
|
+
"src/**/*.json",
|
|
7
|
+
"../../index.d.ts"
|
|
8
|
+
],
|
|
9
|
+
"exclude": [
|
|
10
|
+
"src/**/*.spec.ts",
|
|
11
|
+
"src/**/*.spec.tsx",
|
|
12
|
+
"src/**/*.test.ts",
|
|
13
|
+
"src/**/*.test.tsx"
|
|
14
|
+
],
|
|
5
15
|
"compilerOptions": {
|
|
6
16
|
"emitDeclarationOnly": true,
|
|
7
17
|
"noEmit": false,
|
|
8
18
|
"paths": {
|
|
9
|
-
"@
|
|
19
|
+
"@luxexchange/notifications/*": ["./*"]
|
|
10
20
|
},
|
|
11
21
|
"types": ["node", "vitest/globals"]
|
|
12
22
|
},
|
|
13
23
|
"references": [
|
|
14
24
|
{
|
|
15
|
-
"path": "../
|
|
25
|
+
"path": "../ui"
|
|
16
26
|
},
|
|
17
27
|
{
|
|
18
28
|
"path": "../api"
|
|
19
29
|
},
|
|
20
30
|
{
|
|
21
|
-
"path": "../
|
|
31
|
+
"path": "../eslint-config"
|
|
22
32
|
}
|
|
23
33
|
]
|
|
24
34
|
}
|
package/vitest-setup.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
environment: 'node',
|
|
6
|
+
setupFiles: ['./vitest-setup.ts'],
|
|
7
|
+
coverage: {
|
|
8
|
+
exclude: ['**/__generated__/**', '**/node_modules/**', '**/dist/**', '**/*.config.*', '**/scripts/**'],
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
resolve: {
|
|
12
|
+
extensions: ['.web.ts', '.web.tsx', '.ts', '.tsx', '.js', '.jsx', '.json'],
|
|
13
|
+
},
|
|
14
|
+
})
|