@luxexchange/notifications 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxexchange/notifications",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "dependencies": {
5
5
  "@bufbuild/protobuf": "1.10.0",
6
6
  "@tanstack/react-query": "5.90.20",
@@ -8,12 +8,12 @@
8
8
  "ms": "2.1.3",
9
9
  "react": "19.0.3",
10
10
  "react-i18next": "14.1.0",
11
- "@luxfi/ui": "^1.0.0"
11
+ "@luxfi/ui": "workspace:^"
12
12
  },
13
13
  "devDependencies": {
14
14
  "@types/node": "22.13.1",
15
15
  "@typescript/native-preview": "7.0.0-dev.20260108.1",
16
- "@luxfi/eslint-config": "^1.0.0",
16
+ "@luxfi/eslint-config": "workspace:^",
17
17
  "@vitest/coverage-v8": "3.2.1",
18
18
  "depcheck": "1.4.7",
19
19
  "eslint": "8.57.1",
package/.depcheckrc DELETED
@@ -1,14 +0,0 @@
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 DELETED
@@ -1,20 +0,0 @@
1
- module.exports = {
2
- extends: ['@luxfi/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: '@luxfi/notifications',
15
- },
16
- ],
17
- },
18
- },
19
- ],
20
- }
package/README.md DELETED
@@ -1,548 +0,0 @@
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
-
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "preserveSymlinks": true
5
- },
6
- "include": ["**/*.ts", "**/*.tsx", "**/*.json"],
7
- "exclude": ["node_modules"]
8
- }
package/vitest-setup.ts DELETED
@@ -1 +0,0 @@
1
- export {}
package/vitest.config.ts DELETED
@@ -1,14 +0,0 @@
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
- })