@dismissible/react-client 1.0.0 → 2.0.0-canary.5.88c27fb
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/CHANGELOG.md +30 -0
- package/README.md +298 -186
- package/dist/clients/defaultClient.d.ts +25 -0
- package/dist/config/api.config.d.ts +0 -14
- package/dist/contexts/DismissibleProvider.d.ts +22 -0
- package/dist/dismissible-client.es.js +522 -484
- package/dist/dismissible-client.umd.js +1 -1
- package/dist/hooks/useDismissibleItem.d.ts +17 -17
- package/dist/root.d.ts +1 -5
- package/dist/types/dismissible.types.d.ts +81 -7
- package/dist/utils/url.utils.d.ts +9 -0
- package/package.json +17 -16
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# [2.0.0](https://github.com/DismissibleIo/dismissible-react-client/compare/v1.0.0...v2.0.0) (2026-01-06)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **test:** fixed test race condition ([#6](https://github.com/DismissibleIo/dismissible-react-client/issues/6)) ([da56324](https://github.com/DismissibleIo/dismissible-react-client/commit/da5632458c61a734d7bd55c89ef28d78afeabbf4))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **api:** aligned with api response ([#5](https://github.com/DismissibleIo/dismissible-react-client/issues/5)) ([8fe9f87](https://github.com/DismissibleIo/dismissible-react-client/commit/8fe9f879691f88cb761a8b4293c824a6d4477db2))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### BREAKING CHANGES
|
|
15
|
+
|
|
16
|
+
* **api:** You need to update any of your hook consumers to check dismissedAt instead of
|
|
17
|
+
dismissedOn. This aligns with the API response.
|
|
18
|
+
|
|
19
|
+
# 1.0.0 (2025-12-21)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **react-client:** dismissible React Client ([#1](https://github.com/DismissibleIo/dismissible-react-client/issues/1)) ([a4e6dc7](https://github.com/DismissibleIo/dismissible-react-client/commit/a4e6dc7786a4fd1abb81f4a796e22d37fdc12e0b))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### BREAKING CHANGES
|
|
28
|
+
|
|
29
|
+
* **react-client:** This is the first public release which requires both the DismissibleProvider and
|
|
30
|
+
Dismissible components to be used to force a userId.
|
package/README.md
CHANGED
|
@@ -1,26 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://dismissible.io" target="_blank"><img src="https://raw.githubusercontent.com/DismissibleIo/dismissible-api/main/docs/images/dismissible_logo.png" width="240" alt="Dismissible" /></a>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">Never Show The Same Thing Twice!</p>
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://www.npmjs.com/package/@dismissible/react-client" target="_blank"><img src="https://img.shields.io/npm/v/@dismissible/react-client.svg" alt="NPM Version" /></a>
|
|
8
|
+
<a href="https://github.com/dismissibleio/dismissible-react-client/blob/main/LICENSE" target="_blank"><img src="https://img.shields.io/npm/l/@dismissible/react-client.svg" alt="Package License" /></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@dismissible/react-client" target="_blank"><img src="https://img.shields.io/npm/dm/@dismissible/react-client.svg" alt="NPM Downloads" /></a>
|
|
10
|
+
<a href="https://github.com/dismissibleio/dismissible-react-client" target="_blank"><img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/dismissibleio/dismissible-react-client/publish.yml" /></a>
|
|
11
|
+
<a href="https://paypal.me/joshstuartx" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" /></a>
|
|
12
|
+
</p>
|
|
2
13
|
|
|
3
|
-
|
|
14
|
+
Dismissible manages the state of your UI elements across sessions, so your users see what matters, once! No more onboarding messages reappearing on every tab, no more notifications haunting users across devices. Dismissible syncs dismissal state everywhere, so every message is intentional, never repetitive.
|
|
15
|
+
|
|
16
|
+
# @dismissible/react-client
|
|
4
17
|
|
|
5
|
-
|
|
18
|
+
This is the React component library for creating dismissible UI elements with persistent state management.
|
|
6
19
|
|
|
7
|
-
|
|
20
|
+
This component is used with the [Dismissible API Server](https://github.com/DismissibleIo/dismissible-api), which you can self-host with Docker or integrate into your NestJS application.
|
|
8
21
|
|
|
9
|
-
[
|
|
10
|
-
[](https://opensource.org/licenses/MIT)
|
|
22
|
+
**[dismissible.io](https://dismissible.io)** | **[Documentation](https://dismissible.io/docs)** | **[API Server](https://github.com/DismissibleIo/dismissible-api)**
|
|
11
23
|
|
|
12
24
|
## Features
|
|
13
25
|
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
26
|
+
- **Easy to use** - Simple component API for dismissible content
|
|
27
|
+
- **Persistent state** - Dismissal state is saved and restored across sessions when using the [Dismissible API Server](https://github.com/DismissibleIo/dismissible-api)
|
|
28
|
+
- **Restore support** - Restore previously dismissed items programmatically
|
|
29
|
+
- **JWT Authentication** - Built-in support for secure JWT-based authentication
|
|
30
|
+
- **Custom HTTP Client** - Bring your own HTTP client (axios, ky, etc.) with custom headers, interceptors, and tracking
|
|
31
|
+
- **Customizable** - Custom loading, error, and dismiss button components
|
|
32
|
+
- **Accessible** - Built with accessibility best practices
|
|
33
|
+
- **Hook-based** - Includes `useDismissibleItem` hook for custom implementations
|
|
34
|
+
- **Lightweight** - Minimal bundle size with tree-shaking support
|
|
35
|
+
- **TypeScript** - Full TypeScript support with complete type definitions
|
|
24
36
|
|
|
25
37
|
## Installation
|
|
26
38
|
|
|
@@ -40,7 +52,7 @@ npm install react react-dom
|
|
|
40
52
|
|
|
41
53
|
### 1. Set up the Dismissible API Server
|
|
42
54
|
|
|
43
|
-
First, you need a Dismissible API
|
|
55
|
+
First, you need a [Dismissible API Server](https://github.com/DismissibleIo/dismissible-api). The easiest way is with Docker:
|
|
44
56
|
|
|
45
57
|
```yaml
|
|
46
58
|
# docker-compose.yml
|
|
@@ -52,38 +64,29 @@ services:
|
|
|
52
64
|
- '3001:3001'
|
|
53
65
|
environment:
|
|
54
66
|
DISMISSIBLE_PORT: 3001
|
|
55
|
-
DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING: postgresql://postgres:postgres@db:5432/dismissible
|
|
56
|
-
depends_on:
|
|
57
|
-
- db
|
|
58
|
-
|
|
59
|
-
db:
|
|
60
|
-
image: postgres:15
|
|
61
|
-
environment:
|
|
62
|
-
POSTGRES_USER: postgres
|
|
63
|
-
POSTGRES_PASSWORD: postgres
|
|
64
|
-
POSTGRES_DB: dismissible
|
|
65
|
-
volumes:
|
|
66
|
-
- postgres_data:/var/lib/postgresql/data
|
|
67
|
-
|
|
68
|
-
volumes:
|
|
69
|
-
postgres_data:
|
|
70
67
|
```
|
|
71
68
|
|
|
72
69
|
```bash
|
|
73
70
|
docker-compose up -d
|
|
74
71
|
```
|
|
75
72
|
|
|
76
|
-
|
|
73
|
+
OR
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
docker run -p 3001:3001 -e DISMISSIBLE_PORT=3001 dismissibleio/dismissible-api:latest
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
See the [API Server documentation](https://github.com/DismissibleIo/dismissible-api) for more deployment options including NestJS integration, public Docker image and more.
|
|
77
80
|
|
|
78
81
|
### 2. Configure the Provider
|
|
79
82
|
|
|
80
|
-
Wrap your app with `DismissibleProvider`. The `userId` prop is **required** to track dismissals per user:
|
|
83
|
+
Wrap your app with `DismissibleProvider`. The `userId` prop is **required** to track all your dismissals per user:
|
|
81
84
|
|
|
82
85
|
```tsx
|
|
83
86
|
import { DismissibleProvider } from '@dismissible/react-client';
|
|
84
87
|
|
|
85
88
|
function App() {
|
|
86
|
-
const userId = getCurrentUserId();
|
|
89
|
+
const userId = getCurrentUserId();
|
|
87
90
|
|
|
88
91
|
return (
|
|
89
92
|
<DismissibleProvider userId={userId} baseUrl="http://localhost:3001">
|
|
@@ -94,6 +97,7 @@ function App() {
|
|
|
94
97
|
```
|
|
95
98
|
|
|
96
99
|
### 3. Use Dismissible Components
|
|
100
|
+
Now wrap any component you want to be dismissible with the `<Dismissible>` component, and the `itemId`, along with the `userId`, will become the unique key that is tracked across sessions and devices.
|
|
97
101
|
|
|
98
102
|
```tsx
|
|
99
103
|
import { Dismissible } from '@dismissible/react-client';
|
|
@@ -121,8 +125,9 @@ Context provider that configures authentication and API settings. **Required** -
|
|
|
121
125
|
| Prop | Type | Required | Description |
|
|
122
126
|
|------|------|----------|-------------|
|
|
123
127
|
| `userId` | `string` | ✅ | User ID for tracking dismissals per user |
|
|
128
|
+
| `baseUrl` | `string` | ✅ | API base URL for your self-hosted server |
|
|
124
129
|
| `jwt` | `string \| (() => string) \| (() => Promise<string>)` | ❌ | JWT token for secure authentication |
|
|
125
|
-
| `
|
|
130
|
+
| `client` | `DismissibleClient` | ❌ | Custom HTTP client implementation (uses default if not provided) |
|
|
126
131
|
| `children` | `ReactNode` | ✅ | Components that will use the dismissible functionality |
|
|
127
132
|
|
|
128
133
|
#### Example
|
|
@@ -183,6 +188,8 @@ function AppWithAsyncAuth() {
|
|
|
183
188
|
}
|
|
184
189
|
```
|
|
185
190
|
|
|
191
|
+
See [Custom HTTP Client](#custom-http-client) for examples of using a custom client.
|
|
192
|
+
|
|
186
193
|
### `<Dismissible>` Component
|
|
187
194
|
|
|
188
195
|
The main component for creating dismissible content.
|
|
@@ -242,11 +249,11 @@ For custom implementations and advanced use cases.
|
|
|
242
249
|
|
|
243
250
|
| Property | Type | Description |
|
|
244
251
|
|----------|------|-------------|
|
|
245
|
-
| `
|
|
252
|
+
| `dismissedAt` | `string \| undefined` | ISO date string when item was dismissed, or undefined |
|
|
246
253
|
| `dismiss` | `() => Promise<void>` | Function to dismiss the item |
|
|
247
254
|
| `restore` | `() => Promise<void>` | Function to restore a dismissed item |
|
|
248
255
|
| `isLoading` | `boolean` | Loading state indicator |
|
|
249
|
-
| `error` | `Error \|
|
|
256
|
+
| `error` | `Error \| undefined` | Error state, if any |
|
|
250
257
|
| `item` | `IDismissibleItem \| undefined` | The full dismissible item object |
|
|
251
258
|
|
|
252
259
|
#### Example
|
|
@@ -255,7 +262,7 @@ For custom implementations and advanced use cases.
|
|
|
255
262
|
import { useDismissibleItem } from '@dismissible/react-client';
|
|
256
263
|
|
|
257
264
|
function CustomDismissible({ itemId, children }) {
|
|
258
|
-
const {
|
|
265
|
+
const { dismissedAt, dismiss, restore, isLoading, error } = useDismissibleItem(itemId);
|
|
259
266
|
|
|
260
267
|
if (isLoading) {
|
|
261
268
|
return <div>Loading...</div>;
|
|
@@ -265,7 +272,7 @@ function CustomDismissible({ itemId, children }) {
|
|
|
265
272
|
return <div>Error: {error.message}</div>;
|
|
266
273
|
}
|
|
267
274
|
|
|
268
|
-
if (
|
|
275
|
+
if (dismissedAt) {
|
|
269
276
|
return (
|
|
270
277
|
<div>
|
|
271
278
|
<p>This item was dismissed.</p>
|
|
@@ -338,7 +345,6 @@ function App() {
|
|
|
338
345
|
function Dashboard() {
|
|
339
346
|
return (
|
|
340
347
|
<div>
|
|
341
|
-
{/* Dismissible state is tracked per user */}
|
|
342
348
|
<Dismissible itemId="user-welcome-banner">
|
|
343
349
|
<div className="alert alert-info">
|
|
344
350
|
<h4>Welcome back!</h4>
|
|
@@ -427,19 +433,19 @@ function Dashboard() {
|
|
|
427
433
|
<div>
|
|
428
434
|
<Dismissible itemId="feature-announcement">
|
|
429
435
|
<div className="alert alert-success">
|
|
430
|
-
|
|
436
|
+
New feature: Dark mode is now available!
|
|
431
437
|
</div>
|
|
432
438
|
</Dismissible>
|
|
433
439
|
|
|
434
440
|
<Dismissible itemId="maintenance-notice">
|
|
435
441
|
<div className="alert alert-warning">
|
|
436
|
-
|
|
442
|
+
Scheduled maintenance: Sunday 2AM-4AM EST
|
|
437
443
|
</div>
|
|
438
444
|
</Dismissible>
|
|
439
445
|
|
|
440
446
|
<Dismissible itemId="survey-request">
|
|
441
447
|
<div className="alert alert-info">
|
|
442
|
-
|
|
448
|
+
Help us improve! Take our 2-minute survey.
|
|
443
449
|
</div>
|
|
444
450
|
</Dismissible>
|
|
445
451
|
</div>
|
|
@@ -468,74 +474,6 @@ function RobustBanner() {
|
|
|
468
474
|
}
|
|
469
475
|
```
|
|
470
476
|
|
|
471
|
-
### Integration with Auth Providers
|
|
472
|
-
|
|
473
|
-
```tsx
|
|
474
|
-
import { DismissibleProvider } from '@dismissible/react-client';
|
|
475
|
-
|
|
476
|
-
// With Firebase Auth
|
|
477
|
-
function AppWithFirebase() {
|
|
478
|
-
const user = firebase.auth().currentUser;
|
|
479
|
-
|
|
480
|
-
return (
|
|
481
|
-
<DismissibleProvider
|
|
482
|
-
userId={user.uid}
|
|
483
|
-
jwt={async () => {
|
|
484
|
-
if (user) {
|
|
485
|
-
return await user.getIdToken();
|
|
486
|
-
}
|
|
487
|
-
throw new Error('User not authenticated');
|
|
488
|
-
}}
|
|
489
|
-
baseUrl="https://api.yourapp.com"
|
|
490
|
-
>
|
|
491
|
-
<YourApp />
|
|
492
|
-
</DismissibleProvider>
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// With Auth0
|
|
497
|
-
function AppWithAuth0() {
|
|
498
|
-
const { user, getAccessTokenSilently } = useAuth0();
|
|
499
|
-
|
|
500
|
-
return (
|
|
501
|
-
<DismissibleProvider
|
|
502
|
-
userId={user.sub}
|
|
503
|
-
jwt={async () => await getAccessTokenSilently()}
|
|
504
|
-
baseUrl="https://api.yourapp.com"
|
|
505
|
-
>
|
|
506
|
-
<YourApp />
|
|
507
|
-
</DismissibleProvider>
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// With token refresh logic
|
|
512
|
-
function AppWithTokenRefresh() {
|
|
513
|
-
const { user } = useAuth();
|
|
514
|
-
|
|
515
|
-
return (
|
|
516
|
-
<DismissibleProvider
|
|
517
|
-
userId={user.id}
|
|
518
|
-
jwt={async () => {
|
|
519
|
-
try {
|
|
520
|
-
const response = await fetch('/api/auth/refresh', {
|
|
521
|
-
method: 'POST',
|
|
522
|
-
credentials: 'include'
|
|
523
|
-
});
|
|
524
|
-
const { accessToken } = await response.json();
|
|
525
|
-
return accessToken;
|
|
526
|
-
} catch (error) {
|
|
527
|
-
console.error('Failed to refresh token:', error);
|
|
528
|
-
throw error;
|
|
529
|
-
}
|
|
530
|
-
}}
|
|
531
|
-
baseUrl="https://api.yourapp.com"
|
|
532
|
-
>
|
|
533
|
-
<YourApp />
|
|
534
|
-
</DismissibleProvider>
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
```
|
|
538
|
-
|
|
539
477
|
### Using the Hook for Complex Logic
|
|
540
478
|
|
|
541
479
|
```tsx
|
|
@@ -543,12 +481,12 @@ import { useDismissibleItem } from '@dismissible/react-client';
|
|
|
543
481
|
import { useState, useEffect } from 'react';
|
|
544
482
|
|
|
545
483
|
function SmartNotification({ itemId, message, type = 'info' }) {
|
|
546
|
-
const {
|
|
484
|
+
const { dismissedAt, dismiss, isLoading } = useDismissibleItem(itemId);
|
|
547
485
|
const [autoHide, setAutoHide] = useState(false);
|
|
548
486
|
|
|
549
487
|
// Auto-hide after 10 seconds for info messages
|
|
550
488
|
useEffect(() => {
|
|
551
|
-
if (type === 'info' && !
|
|
489
|
+
if (type === 'info' && !dismissedAt) {
|
|
552
490
|
const timer = setTimeout(() => {
|
|
553
491
|
setAutoHide(true);
|
|
554
492
|
dismiss();
|
|
@@ -556,9 +494,9 @@ function SmartNotification({ itemId, message, type = 'info' }) {
|
|
|
556
494
|
|
|
557
495
|
return () => clearTimeout(timer);
|
|
558
496
|
}
|
|
559
|
-
}, [type,
|
|
497
|
+
}, [type, dismissedAt, dismiss]);
|
|
560
498
|
|
|
561
|
-
if (
|
|
499
|
+
if (dismissedAt || autoHide) {
|
|
562
500
|
return null;
|
|
563
501
|
}
|
|
564
502
|
|
|
@@ -585,12 +523,12 @@ Use the `restore` function to bring back previously dismissed content:
|
|
|
585
523
|
import { useDismissibleItem } from '@dismissible/react-client';
|
|
586
524
|
|
|
587
525
|
function RestorableBanner({ itemId }) {
|
|
588
|
-
const {
|
|
526
|
+
const { dismissedAt, dismiss, restore, isLoading } = useDismissibleItem(itemId);
|
|
589
527
|
|
|
590
|
-
if (
|
|
528
|
+
if (dismissedAt) {
|
|
591
529
|
return (
|
|
592
530
|
<div className="dismissed-placeholder">
|
|
593
|
-
<p>Banner was dismissed on {new Date(
|
|
531
|
+
<p>Banner was dismissed on {new Date(dismissedAt).toLocaleDateString()}</p>
|
|
594
532
|
<button onClick={restore} disabled={isLoading}>
|
|
595
533
|
{isLoading ? 'Restoring...' : 'Show Banner Again'}
|
|
596
534
|
</button>
|
|
@@ -610,51 +548,254 @@ function RestorableBanner({ itemId }) {
|
|
|
610
548
|
}
|
|
611
549
|
```
|
|
612
550
|
|
|
613
|
-
|
|
551
|
+
## Advanced Usage
|
|
552
|
+
|
|
553
|
+
### Custom HTTP Client
|
|
554
|
+
|
|
555
|
+
By default, Dismissible uses a built-in HTTP client powered by `openapi-fetch`. However, you can provide your own HTTP client implementation by passing a `client` prop to the `DismissibleProvider`. This is useful when you need:
|
|
556
|
+
|
|
557
|
+
- Custom headers (correlation IDs, tracing, etc.)
|
|
558
|
+
- Request/response interceptors
|
|
559
|
+
- Use a different HTTP library (axios, ky, etc.)
|
|
560
|
+
- Analytics and logging
|
|
561
|
+
- Custom error handling
|
|
562
|
+
|
|
563
|
+
#### The DismissibleClient Interface
|
|
564
|
+
|
|
565
|
+
Your custom client must implement the `DismissibleClient` interface:
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
import type { DismissibleClient, DismissibleItem } from '@dismissible/react-client';
|
|
569
|
+
|
|
570
|
+
interface DismissibleClient {
|
|
571
|
+
getOrCreate: (params: {
|
|
572
|
+
userId: string;
|
|
573
|
+
itemId: string;
|
|
574
|
+
baseUrl: string;
|
|
575
|
+
authHeaders: { Authorization?: string };
|
|
576
|
+
signal?: AbortSignal;
|
|
577
|
+
}) => Promise<DismissibleItem>;
|
|
578
|
+
|
|
579
|
+
dismiss: (params: {
|
|
580
|
+
userId: string;
|
|
581
|
+
itemId: string;
|
|
582
|
+
baseUrl: string;
|
|
583
|
+
authHeaders: { Authorization?: string };
|
|
584
|
+
}) => Promise<DismissibleItem>;
|
|
585
|
+
|
|
586
|
+
restore: (params: {
|
|
587
|
+
userId: string;
|
|
588
|
+
itemId: string;
|
|
589
|
+
baseUrl: string;
|
|
590
|
+
authHeaders: { Authorization?: string };
|
|
591
|
+
}) => Promise<DismissibleItem>;
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
#### Example: Custom Client with Axios
|
|
614
596
|
|
|
615
597
|
```tsx
|
|
616
|
-
import
|
|
598
|
+
import axios from 'axios';
|
|
599
|
+
import { v4 as uuid } from 'uuid';
|
|
600
|
+
import { DismissibleProvider } from '@dismissible/react-client';
|
|
601
|
+
import type { DismissibleClient } from '@dismissible/react-client';
|
|
602
|
+
|
|
603
|
+
const axiosClient: DismissibleClient = {
|
|
604
|
+
getOrCreate: async ({ userId, itemId, baseUrl, authHeaders, signal }) => {
|
|
605
|
+
const response = await axios.get(
|
|
606
|
+
`${baseUrl}/v1/users/${userId}/items/${itemId}`,
|
|
607
|
+
{
|
|
608
|
+
headers: {
|
|
609
|
+
...authHeaders,
|
|
610
|
+
'X-Correlation-ID': uuid(),
|
|
611
|
+
},
|
|
612
|
+
signal,
|
|
613
|
+
}
|
|
614
|
+
);
|
|
615
|
+
return response.data.data;
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
dismiss: async ({ userId, itemId, baseUrl, authHeaders }) => {
|
|
619
|
+
const response = await axios.delete(
|
|
620
|
+
`${baseUrl}/v1/users/${userId}/items/${itemId}`,
|
|
621
|
+
{
|
|
622
|
+
headers: {
|
|
623
|
+
...authHeaders,
|
|
624
|
+
'X-Correlation-ID': uuid(),
|
|
625
|
+
},
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
return response.data.data;
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
restore: async ({ userId, itemId, baseUrl, authHeaders }) => {
|
|
632
|
+
const response = await axios.post(
|
|
633
|
+
`${baseUrl}/v1/users/${userId}/items/${itemId}`,
|
|
634
|
+
{},
|
|
635
|
+
{
|
|
636
|
+
headers: {
|
|
637
|
+
...authHeaders,
|
|
638
|
+
'X-Correlation-ID': uuid(),
|
|
639
|
+
},
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
return response.data.data;
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
function App() {
|
|
647
|
+
return (
|
|
648
|
+
<DismissibleProvider
|
|
649
|
+
userId="user-123"
|
|
650
|
+
baseUrl="https://api.yourapp.com"
|
|
651
|
+
client={axiosClient}
|
|
652
|
+
>
|
|
653
|
+
<YourApp />
|
|
654
|
+
</DismissibleProvider>
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
```
|
|
617
658
|
|
|
618
|
-
|
|
619
|
-
const { dismissedOn, dismiss, restore, item, isLoading, error } =
|
|
620
|
-
useDismissibleItem(notificationId);
|
|
659
|
+
#### Example: Custom Client with Logging
|
|
621
660
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
661
|
+
```tsx
|
|
662
|
+
import type { DismissibleClient } from '@dismissible/react-client';
|
|
663
|
+
|
|
664
|
+
const loggingClient: DismissibleClient = {
|
|
665
|
+
getOrCreate: async ({ userId, itemId, baseUrl, authHeaders, signal }) => {
|
|
666
|
+
console.log(`[Dismissible] Fetching item: ${itemId} for user: ${userId}`);
|
|
667
|
+
const startTime = performance.now();
|
|
668
|
+
|
|
669
|
+
const response = await fetch(
|
|
670
|
+
`${baseUrl}/v1/users/${userId}/items/${itemId}`,
|
|
671
|
+
{
|
|
672
|
+
method: 'GET',
|
|
673
|
+
headers: authHeaders,
|
|
674
|
+
signal,
|
|
675
|
+
}
|
|
676
|
+
);
|
|
625
677
|
|
|
678
|
+
const data = await response.json();
|
|
679
|
+
console.log(`[Dismissible] Fetched in ${performance.now() - startTime}ms`);
|
|
680
|
+
|
|
681
|
+
if (!response.ok) {
|
|
682
|
+
throw new Error(data.message || 'Failed to fetch dismissible item');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return data.data;
|
|
686
|
+
},
|
|
687
|
+
|
|
688
|
+
dismiss: async ({ userId, itemId, baseUrl, authHeaders }) => {
|
|
689
|
+
console.log(`[Dismissible] Dismissing item: ${itemId}`);
|
|
690
|
+
|
|
691
|
+
const response = await fetch(
|
|
692
|
+
`${baseUrl}/v1/users/${userId}/items/${itemId}`,
|
|
693
|
+
{
|
|
694
|
+
method: 'DELETE',
|
|
695
|
+
headers: authHeaders,
|
|
696
|
+
}
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
const data = await response.json();
|
|
700
|
+
|
|
701
|
+
if (!response.ok) {
|
|
702
|
+
throw new Error(data.message || 'Failed to dismiss item');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
console.log(`[Dismissible] Item dismissed at: ${data.data.dismissedAt}`);
|
|
706
|
+
return data.data;
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
restore: async ({ userId, itemId, baseUrl, authHeaders }) => {
|
|
710
|
+
console.log(`[Dismissible] Restoring item: ${itemId}`);
|
|
711
|
+
|
|
712
|
+
const response = await fetch(
|
|
713
|
+
`${baseUrl}/v1/users/${userId}/items/${itemId}`,
|
|
714
|
+
{
|
|
715
|
+
method: 'POST',
|
|
716
|
+
headers: authHeaders,
|
|
717
|
+
}
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
const data = await response.json();
|
|
721
|
+
|
|
722
|
+
if (!response.ok) {
|
|
723
|
+
throw new Error(data.message || 'Failed to restore item');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
console.log(`[Dismissible] Item restored successfully`);
|
|
727
|
+
return data.data;
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
function App() {
|
|
626
732
|
return (
|
|
627
|
-
<
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
{
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
<div className="actions">
|
|
635
|
-
{dismissedOn ? (
|
|
636
|
-
<button
|
|
637
|
-
onClick={restore}
|
|
638
|
-
disabled={isLoading}
|
|
639
|
-
className="btn-restore"
|
|
640
|
-
>
|
|
641
|
-
Restore
|
|
642
|
-
</button>
|
|
643
|
-
) : (
|
|
644
|
-
<button
|
|
645
|
-
onClick={dismiss}
|
|
646
|
-
disabled={isLoading}
|
|
647
|
-
className="btn-dismiss"
|
|
648
|
-
>
|
|
649
|
-
Dismiss
|
|
650
|
-
</button>
|
|
651
|
-
)}
|
|
652
|
-
</div>
|
|
653
|
-
</div>
|
|
733
|
+
<DismissibleProvider
|
|
734
|
+
userId="user-123"
|
|
735
|
+
baseUrl="https://api.yourapp.com"
|
|
736
|
+
client={loggingClient}
|
|
737
|
+
>
|
|
738
|
+
<YourApp />
|
|
739
|
+
</DismissibleProvider>
|
|
654
740
|
);
|
|
655
741
|
}
|
|
656
742
|
```
|
|
657
743
|
|
|
744
|
+
#### Example: Custom Client with Retry Logic
|
|
745
|
+
|
|
746
|
+
```tsx
|
|
747
|
+
import type { DismissibleClient } from '@dismissible/react-client';
|
|
748
|
+
|
|
749
|
+
async function fetchWithRetry(
|
|
750
|
+
url: string,
|
|
751
|
+
options: RequestInit,
|
|
752
|
+
retries = 3
|
|
753
|
+
): Promise<Response> {
|
|
754
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
755
|
+
try {
|
|
756
|
+
const response = await fetch(url, options);
|
|
757
|
+
if (response.ok || attempt === retries) {
|
|
758
|
+
return response;
|
|
759
|
+
}
|
|
760
|
+
} catch (error) {
|
|
761
|
+
if (attempt === retries) throw error;
|
|
762
|
+
}
|
|
763
|
+
// Exponential backoff
|
|
764
|
+
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
765
|
+
}
|
|
766
|
+
throw new Error('Max retries reached');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const retryClient: DismissibleClient = {
|
|
770
|
+
getOrCreate: async ({ userId, itemId, baseUrl, authHeaders, signal }) => {
|
|
771
|
+
const response = await fetchWithRetry(
|
|
772
|
+
`${baseUrl}/v1/users/${userId}/items/${itemId}`,
|
|
773
|
+
{ method: 'GET', headers: authHeaders, signal }
|
|
774
|
+
);
|
|
775
|
+
const data = await response.json();
|
|
776
|
+
return data.data;
|
|
777
|
+
},
|
|
778
|
+
|
|
779
|
+
dismiss: async ({ userId, itemId, baseUrl, authHeaders }) => {
|
|
780
|
+
const response = await fetchWithRetry(
|
|
781
|
+
`${baseUrl}/v1/users/${userId}/items/${itemId}`,
|
|
782
|
+
{ method: 'DELETE', headers: authHeaders }
|
|
783
|
+
);
|
|
784
|
+
const data = await response.json();
|
|
785
|
+
return data.data;
|
|
786
|
+
},
|
|
787
|
+
|
|
788
|
+
restore: async ({ userId, itemId, baseUrl, authHeaders }) => {
|
|
789
|
+
const response = await fetchWithRetry(
|
|
790
|
+
`${baseUrl}/v1/users/${userId}/items/${itemId}`,
|
|
791
|
+
{ method: 'POST', headers: authHeaders }
|
|
792
|
+
);
|
|
793
|
+
const data = await response.json();
|
|
794
|
+
return data.data;
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
```
|
|
798
|
+
|
|
658
799
|
## Styling
|
|
659
800
|
|
|
660
801
|
The library includes minimal default styles. You can override them or provide your own:
|
|
@@ -678,35 +819,6 @@ The library includes minimal default styles. You can override them or provide yo
|
|
|
678
819
|
}
|
|
679
820
|
```
|
|
680
821
|
|
|
681
|
-
## TypeScript Support
|
|
682
|
-
|
|
683
|
-
The library is written in TypeScript and exports all type definitions:
|
|
684
|
-
|
|
685
|
-
```tsx
|
|
686
|
-
import type {
|
|
687
|
-
DismissibleProps,
|
|
688
|
-
DismissibleProviderProps,
|
|
689
|
-
JwtToken,
|
|
690
|
-
} from '@dismissible/react-client';
|
|
691
|
-
|
|
692
|
-
// Custom provider wrapper
|
|
693
|
-
const AuthenticatedDismissibleProvider: React.FC<{
|
|
694
|
-
children: React.ReactNode;
|
|
695
|
-
}> = ({ children }) => {
|
|
696
|
-
const { user, getToken } = useAuth();
|
|
697
|
-
|
|
698
|
-
return (
|
|
699
|
-
<DismissibleProvider
|
|
700
|
-
userId={user.id}
|
|
701
|
-
jwt={getToken}
|
|
702
|
-
baseUrl={process.env.DISMISSIBLE_API_URL}
|
|
703
|
-
>
|
|
704
|
-
{children}
|
|
705
|
-
</DismissibleProvider>
|
|
706
|
-
);
|
|
707
|
-
};
|
|
708
|
-
```
|
|
709
|
-
|
|
710
822
|
## Self-Hosting
|
|
711
823
|
|
|
712
824
|
Dismissible is designed to be self-hosted. You have full control over your data.
|
|
@@ -733,9 +845,9 @@ See the [NestJS documentation](https://dismissible.io/docs/nestjs) for setup ins
|
|
|
733
845
|
|
|
734
846
|
## Support
|
|
735
847
|
|
|
736
|
-
-
|
|
737
|
-
-
|
|
738
|
-
-
|
|
848
|
+
- [Documentation](https://dismissible.io/docs)
|
|
849
|
+
- [GitHub - React Client](https://github.com/DismissibleIo/dismissible-react-client)
|
|
850
|
+
- [GitHub - API Server](https://github.com/DismissibleIo/dismissible-api)
|
|
739
851
|
|
|
740
852
|
## License
|
|
741
853
|
|