@duvandroid/react-blind-agents 0.1.0 → 0.1.1
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/LICENSE +21 -0
- package/README.md +624 -45
- package/package.json +9 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 blind-creator-inc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,26 +1,81 @@
|
|
|
1
|
-
# react-blind-agents
|
|
1
|
+
# @duvandroid/react-blind-agents
|
|
2
2
|
|
|
3
3
|
React component for the [Blind Agents](https://blindagents.com) pixel widget — AI bug reporter, webchat, and product guides.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/@duvandroid/react-blind-agents)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
- [Installation](#installation)
|
|
13
|
+
- [Quick Start](#quick-start)
|
|
14
|
+
- [React (Vite / CRA)](#react-vite--cra)
|
|
15
|
+
- [Next.js — App Router](#nextjs--app-router)
|
|
16
|
+
- [Next.js — Pages Router](#nextjs--pages-router)
|
|
17
|
+
- [Authenticated users](#authenticated-users)
|
|
18
|
+
- [Props reference](#props-reference)
|
|
19
|
+
- [HTML / Any website (script tag)](#html--any-website-script-tag)
|
|
20
|
+
- [Shopify](#shopify)
|
|
21
|
+
- [Lovable](#lovable)
|
|
22
|
+
- [Wix](#wix)
|
|
23
|
+
- [WordPress](#wordpress)
|
|
24
|
+
- [Webflow](#webflow)
|
|
25
|
+
- [Squarespace](#squarespace)
|
|
26
|
+
- [Ghost](#ghost)
|
|
27
|
+
- [Bubble](#bubble)
|
|
28
|
+
- [Framer](#framer)
|
|
29
|
+
- [Google Tag Manager](#google-tag-manager)
|
|
30
|
+
- [Webhooks — Slack integration](#webhooks--slack-integration)
|
|
31
|
+
- [Webhooks — n8n integration](#webhooks--n8n-integration)
|
|
32
|
+
- [Webhook payload reference](#webhook-payload-reference)
|
|
33
|
+
- [Signature verification](#signature-verification)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
5
37
|
## Installation
|
|
6
38
|
|
|
7
39
|
```bash
|
|
8
|
-
npm install react-blind-agents
|
|
40
|
+
npm install @duvandroid/react-blind-agents
|
|
9
41
|
```
|
|
10
42
|
|
|
11
|
-
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
Paste this before `</body>` in any HTML file — no npm needed:
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<script
|
|
51
|
+
src="https://cdn.blindagents.com/report.js"
|
|
52
|
+
data-api-key="YOUR_API_KEY"
|
|
53
|
+
data-primary-color="#e11d48"
|
|
54
|
+
data-title="Help Center"
|
|
55
|
+
data-report-btn-text="Report an issue"
|
|
56
|
+
data-btn-emoji="🔴"
|
|
57
|
+
data-btn-tooltip="Report an issue"
|
|
58
|
+
data-empty-text="No issues reported yet."
|
|
59
|
+
data-user-whatsapp="">
|
|
60
|
+
</script>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> Get your API key from **Blind Agents → Settings → API Keys**.
|
|
64
|
+
|
|
65
|
+
---
|
|
12
66
|
|
|
13
|
-
|
|
67
|
+
## React (Vite / CRA)
|
|
14
68
|
|
|
15
69
|
```tsx
|
|
16
|
-
|
|
70
|
+
// src/App.tsx
|
|
71
|
+
import { BlindAgentsWidget } from '@duvandroid/react-blind-agents';
|
|
17
72
|
|
|
18
73
|
export default function App() {
|
|
19
74
|
return (
|
|
20
75
|
<>
|
|
21
76
|
<MyRoutes />
|
|
22
77
|
<BlindAgentsWidget
|
|
23
|
-
apiKey="
|
|
78
|
+
apiKey="YOUR_API_KEY"
|
|
24
79
|
primaryColor="#e11d48"
|
|
25
80
|
title="Help Center"
|
|
26
81
|
reportBtnText="Report an issue"
|
|
@@ -33,11 +88,17 @@ export default function App() {
|
|
|
33
88
|
}
|
|
34
89
|
```
|
|
35
90
|
|
|
36
|
-
|
|
91
|
+
Place `<BlindAgentsWidget>` once at the App root — it renders nothing in the DOM, only injects the script.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Next.js — App Router
|
|
96
|
+
|
|
97
|
+
Import from the `/next` subpath — it uses `next/script` internally for correct hydration and strategy support.
|
|
37
98
|
|
|
38
99
|
```tsx
|
|
39
100
|
// app/layout.tsx
|
|
40
|
-
import { BlindAgentsWidget } from 'react-blind-agents/next';
|
|
101
|
+
import { BlindAgentsWidget } from '@duvandroid/react-blind-agents/next';
|
|
41
102
|
|
|
42
103
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
43
104
|
return (
|
|
@@ -45,7 +106,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
45
106
|
<body>
|
|
46
107
|
{children}
|
|
47
108
|
<BlindAgentsWidget
|
|
48
|
-
apiKey="
|
|
109
|
+
apiKey="YOUR_API_KEY"
|
|
49
110
|
primaryColor="#e11d48"
|
|
50
111
|
title="Help Center"
|
|
51
112
|
reportBtnText="Report an issue"
|
|
@@ -58,67 +119,585 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
58
119
|
}
|
|
59
120
|
```
|
|
60
121
|
|
|
61
|
-
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Next.js — Pages Router
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
// pages/_app.tsx
|
|
128
|
+
import type { AppProps } from 'next/app';
|
|
129
|
+
import { BlindAgentsWidget } from '@duvandroid/react-blind-agents/next';
|
|
130
|
+
|
|
131
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
<Component {...pageProps} />
|
|
135
|
+
<BlindAgentsWidget apiKey="YOUR_API_KEY" primaryColor="#e11d48" />
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Authenticated users
|
|
144
|
+
|
|
145
|
+
Pass the logged-in user's phone or email via `userWhatsapp` to skip the identity verification step inside the widget:
|
|
62
146
|
|
|
63
147
|
```tsx
|
|
64
148
|
// components/ReportWidget.tsx
|
|
65
149
|
'use client';
|
|
66
|
-
import { BlindAgentsWidget } from 'react-blind-agents/next';
|
|
150
|
+
import { BlindAgentsWidget } from '@duvandroid/react-blind-agents/next';
|
|
67
151
|
import { useAuth } from './AuthProvider';
|
|
68
152
|
|
|
69
153
|
export function ReportWidget() {
|
|
70
154
|
const { user } = useAuth();
|
|
71
155
|
return (
|
|
72
156
|
<BlindAgentsWidget
|
|
73
|
-
apiKey="
|
|
157
|
+
apiKey="YOUR_API_KEY"
|
|
74
158
|
primaryColor="#e11d48"
|
|
75
|
-
userWhatsapp={user?.
|
|
159
|
+
userWhatsapp={user?.phone ?? user?.email ?? ''}
|
|
76
160
|
/>
|
|
77
161
|
);
|
|
78
162
|
}
|
|
79
163
|
```
|
|
80
164
|
|
|
81
|
-
|
|
165
|
+
Place `<ReportWidget />` **inside** your `AuthProvider` tree so `useAuth()` has access to the context.
|
|
82
166
|
|
|
83
|
-
|
|
84
|
-
// pages/_app.tsx
|
|
85
|
-
import type { AppProps } from 'next/app';
|
|
86
|
-
import { BlindAgentsWidget } from 'react-blind-agents/next';
|
|
167
|
+
---
|
|
87
168
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
169
|
+
## Props reference
|
|
170
|
+
|
|
171
|
+
| Prop | Type | Required | Default | Description |
|
|
172
|
+
|---|---|---|---|---|
|
|
173
|
+
| `apiKey` | `string` | ✅ | — | Your Blind Agents public API key (`ba_...`) |
|
|
174
|
+
| `primaryColor` | `string` | — | — | Accent color (any valid CSS color) |
|
|
175
|
+
| `title` | `string` | — | `"Help Center"` | Widget panel header title |
|
|
176
|
+
| `reportBtnText` | `string` | — | `"Report an issue"` | Report button label |
|
|
177
|
+
| `btnEmoji` | `string` | — | — | Emoji on the floating launcher button |
|
|
178
|
+
| `btnTooltip` | `string` | — | — | Tooltip on the launcher button |
|
|
179
|
+
| `emptyText` | `string` | — | `"No issues reported yet."` | Text shown when there are no reports |
|
|
180
|
+
| `userWhatsapp` | `string` | — | — | Pre-fill user phone/email — skips identity verification |
|
|
181
|
+
| `strategy` | `"afterInteractive" \| "lazyOnload" \| "beforeInteractive"` | — | `"afterInteractive"` | Script loading strategy |
|
|
182
|
+
| `src` | `string` | — | `"https://cdn.blindagents.com/report.js"` | Override CDN URL (self-hosting) |
|
|
183
|
+
| `onLoad` | `() => void` | — | — | Called when the script loads |
|
|
184
|
+
| `onError` | `(error: Error) => void` | — | — | Called if the script fails to load |
|
|
185
|
+
|
|
186
|
+
### Why two import paths?
|
|
187
|
+
|
|
188
|
+
| Import | Use case |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `@duvandroid/react-blind-agents` | Plain React — Vite, CRA, Remix, any non-Next.js |
|
|
191
|
+
| `@duvandroid/react-blind-agents/next` | Next.js — App Router & Pages Router |
|
|
192
|
+
|
|
193
|
+
Importing `/next` in a non-Next.js project will throw because `next/script` won't be available.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## HTML / Any website (script tag)
|
|
198
|
+
|
|
199
|
+
No npm required. Paste before `</body>`:
|
|
200
|
+
|
|
201
|
+
```html
|
|
202
|
+
<script
|
|
203
|
+
src="https://cdn.blindagents.com/report.js"
|
|
204
|
+
data-api-key="YOUR_API_KEY"
|
|
205
|
+
data-primary-color="#e11d48"
|
|
206
|
+
data-title="Help Center"
|
|
207
|
+
data-report-btn-text="Report an issue"
|
|
208
|
+
data-btn-emoji="🔴"
|
|
209
|
+
data-btn-tooltip="Report an issue"
|
|
210
|
+
data-empty-text="No issues reported yet."
|
|
211
|
+
data-user-whatsapp="">
|
|
212
|
+
</script>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The `data-*` attribute names map 1:1 to the React props (kebab-case → camelCase).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Shopify
|
|
220
|
+
|
|
221
|
+
1. **Online Store → Themes → Edit code → Layout → theme.liquid**
|
|
222
|
+
2. Paste before `</body>`:
|
|
223
|
+
|
|
224
|
+
```liquid
|
|
225
|
+
{%- comment -%} Blind Agents widget {%- endcomment -%}
|
|
226
|
+
<script
|
|
227
|
+
src="https://cdn.blindagents.com/report.js"
|
|
228
|
+
data-api-key="YOUR_API_KEY"
|
|
229
|
+
data-primary-color="#e11d48"
|
|
230
|
+
data-title="Help Center"
|
|
231
|
+
data-report-btn-text="Report an issue"
|
|
232
|
+
data-btn-emoji="🔴"
|
|
233
|
+
data-btn-tooltip="Report an issue"
|
|
234
|
+
data-user-whatsapp="{{ customer.phone | default: '' }}">
|
|
235
|
+
</script>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The `{{ customer.phone }}` Liquid variable auto-fills logged-in customer's phone — skipping identity verification for authenticated shoppers.
|
|
239
|
+
|
|
240
|
+
For Shopify Plus headless stores (Hydrogen / Remix), use the React npm package instead.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Lovable
|
|
245
|
+
|
|
246
|
+
**Option A — Prompt Lovable:**
|
|
247
|
+
> "Install @duvandroid/react-blind-agents and add a BlindAgentsWidget to App.tsx with apiKey='YOUR_API_KEY' and primaryColor='#e11d48'"
|
|
248
|
+
|
|
249
|
+
**Option B — Manual (index.html):**
|
|
250
|
+
```html
|
|
251
|
+
<!-- Paste in index.html before </body> -->
|
|
252
|
+
<script
|
|
253
|
+
src="https://cdn.blindagents.com/report.js"
|
|
254
|
+
data-api-key="YOUR_API_KEY"
|
|
255
|
+
data-primary-color="#e11d48"
|
|
256
|
+
data-title="Help Center"
|
|
257
|
+
data-report-btn-text="Report an issue"
|
|
258
|
+
data-btn-emoji="🔴">
|
|
259
|
+
</script>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Wix
|
|
265
|
+
|
|
266
|
+
**Option A — Custom Code (no-code, recommended):**
|
|
267
|
+
1. **Settings → Custom Code → + Add Custom Code**
|
|
268
|
+
2. Paste the script tag, set placement to **Body – end**, apply to **All Pages**, load **Once**
|
|
269
|
+
|
|
270
|
+
**Option B — Velo:**
|
|
271
|
+
```js
|
|
272
|
+
$w.onReady(() => {
|
|
273
|
+
const script = document.createElement('script');
|
|
274
|
+
script.src = 'https://cdn.blindagents.com/report.js';
|
|
275
|
+
script.setAttribute('data-api-key', 'YOUR_API_KEY');
|
|
276
|
+
script.setAttribute('data-primary-color', '#e11d48');
|
|
277
|
+
script.setAttribute('data-title', 'Help Center');
|
|
278
|
+
script.defer = true;
|
|
279
|
+
document.body.appendChild(script);
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## WordPress
|
|
286
|
+
|
|
287
|
+
**Via functions.php (child theme):**
|
|
288
|
+
```php
|
|
289
|
+
function blindagents_widget() {
|
|
290
|
+
echo '<script
|
|
291
|
+
src="https://cdn.blindagents.com/report.js"
|
|
292
|
+
data-api-key="YOUR_API_KEY"
|
|
293
|
+
data-primary-color="#e11d48"
|
|
294
|
+
data-title="Help Center"
|
|
295
|
+
data-report-btn-text="Report an issue"
|
|
296
|
+
data-btn-emoji="🔴"
|
|
297
|
+
defer>
|
|
298
|
+
</script>';
|
|
299
|
+
}
|
|
300
|
+
add_action('wp_footer', 'blindagents_widget');
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Via plugin (no-code):** Install **WPCode** or **Insert Headers and Footers**, paste the script tag in the Footer section.
|
|
304
|
+
|
|
305
|
+
**Via Elementor:** Custom Code → Body End.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Webflow
|
|
310
|
+
|
|
311
|
+
**Site Settings → Custom Code → Footer Code:**
|
|
312
|
+
```html
|
|
313
|
+
<script
|
|
314
|
+
src="https://cdn.blindagents.com/report.js"
|
|
315
|
+
data-api-key="YOUR_API_KEY"
|
|
316
|
+
data-primary-color="#e11d48"
|
|
317
|
+
data-title="Help Center"
|
|
318
|
+
data-report-btn-text="Report an issue"
|
|
319
|
+
data-btn-emoji="🔴"
|
|
320
|
+
defer>
|
|
321
|
+
</script>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Squarespace
|
|
327
|
+
|
|
328
|
+
> Requires Business plan or above.
|
|
329
|
+
|
|
330
|
+
**Settings → Advanced → Code Injection → Footer:**
|
|
331
|
+
```html
|
|
332
|
+
<script
|
|
333
|
+
src="https://cdn.blindagents.com/report.js"
|
|
334
|
+
data-api-key="YOUR_API_KEY"
|
|
335
|
+
data-primary-color="#e11d48"
|
|
336
|
+
data-title="Help Center"
|
|
337
|
+
data-report-btn-text="Report an issue"
|
|
338
|
+
data-btn-emoji="🔴"
|
|
339
|
+
defer>
|
|
340
|
+
</script>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Ghost
|
|
346
|
+
|
|
347
|
+
**Admin → Settings → Code Injection → Site Footer:**
|
|
348
|
+
```html
|
|
349
|
+
<script
|
|
350
|
+
src="https://cdn.blindagents.com/report.js"
|
|
351
|
+
data-api-key="YOUR_API_KEY"
|
|
352
|
+
data-primary-color="#e11d48"
|
|
353
|
+
data-title="Help Center"
|
|
354
|
+
data-report-btn-text="Report an issue"
|
|
355
|
+
data-btn-emoji="🔴"
|
|
356
|
+
defer>
|
|
357
|
+
</script>
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Bubble
|
|
363
|
+
|
|
364
|
+
**Settings → SEO / metatags → Script/meta tags in header:**
|
|
365
|
+
```html
|
|
366
|
+
<script
|
|
367
|
+
src="https://cdn.blindagents.com/report.js"
|
|
368
|
+
data-api-key="YOUR_API_KEY"
|
|
369
|
+
data-primary-color="#e11d48"
|
|
370
|
+
data-title="Help Center"
|
|
371
|
+
data-report-btn-text="Report an issue"
|
|
372
|
+
data-btn-emoji="🔴"
|
|
373
|
+
defer>
|
|
374
|
+
</script>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Or add an **HTML element** on any page and paste the script tag there.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Framer
|
|
382
|
+
|
|
383
|
+
**Site Settings → General → Custom Code → End of body tag:**
|
|
384
|
+
```html
|
|
385
|
+
<script
|
|
386
|
+
src="https://cdn.blindagents.com/report.js"
|
|
387
|
+
data-api-key="YOUR_API_KEY"
|
|
388
|
+
data-primary-color="#e11d48"
|
|
389
|
+
data-title="Help Center"
|
|
390
|
+
data-report-btn-text="Report an issue"
|
|
391
|
+
data-btn-emoji="🔴"
|
|
392
|
+
defer>
|
|
393
|
+
</script>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
> Requires a Framer paid plan.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Google Tag Manager
|
|
401
|
+
|
|
402
|
+
1. **Tags → New → Custom HTML**
|
|
403
|
+
2. Paste:
|
|
404
|
+
|
|
405
|
+
```html
|
|
406
|
+
<script>
|
|
407
|
+
(function() {
|
|
408
|
+
var el = document.createElement('script');
|
|
409
|
+
el.src = 'https://cdn.blindagents.com/report.js';
|
|
410
|
+
el.setAttribute('data-api-key', 'YOUR_API_KEY');
|
|
411
|
+
el.setAttribute('data-primary-color', '#e11d48');
|
|
412
|
+
el.setAttribute('data-title', 'Help Center');
|
|
413
|
+
el.setAttribute('data-report-btn-text', 'Report an issue');
|
|
414
|
+
el.setAttribute('data-btn-emoji', '🔴');
|
|
415
|
+
el.defer = true;
|
|
416
|
+
document.head.appendChild(el);
|
|
417
|
+
})();
|
|
418
|
+
</script>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
3. Trigger: **All Pages — Page View**
|
|
422
|
+
4. Submit and publish
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Webhooks — Slack integration
|
|
427
|
+
|
|
428
|
+
Get real-time Slack notifications for every Blind Agents event.
|
|
429
|
+
|
|
430
|
+
### 1. Create a Slack Incoming Webhook
|
|
431
|
+
|
|
432
|
+
1. Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App → From scratch**
|
|
433
|
+
2. **Features → Incoming Webhooks** → enable → **Add New Webhook to Workspace**
|
|
434
|
+
3. Select the channel (e.g. `#bugs`) and copy the webhook URL
|
|
435
|
+
|
|
436
|
+
Test it:
|
|
437
|
+
```bash
|
|
438
|
+
curl -X POST https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK \
|
|
439
|
+
-H "Content-Type: application/json" \
|
|
440
|
+
-d '{"text": "Blind Agents test message ✅"}'
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### 2. Create a Blind Agents webhook
|
|
444
|
+
|
|
445
|
+
In **Blind Agents → Webhooks → Add webhook**, set the URL to your server endpoint and select events.
|
|
446
|
+
|
|
447
|
+
### 3. Forward events to Slack (Node.js / Express)
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
import express from 'express';
|
|
451
|
+
import crypto from 'crypto';
|
|
452
|
+
|
|
453
|
+
const app = express();
|
|
454
|
+
app.use(express.raw({ type: 'application/json' })); // raw body required for signature check
|
|
455
|
+
|
|
456
|
+
const BA_SECRET = process.env.BLIND_AGENTS_WEBHOOK_SECRET;
|
|
457
|
+
const SLACK_URL = process.env.SLACK_WEBHOOK_URL;
|
|
458
|
+
|
|
459
|
+
function verify(rawBody: string, signature: string) {
|
|
460
|
+
const parts = Object.fromEntries(signature.split(',').map(p => p.split('=')));
|
|
461
|
+
const expected = crypto.createHmac('sha256', BA_SECRET!)
|
|
462
|
+
.update(`${parts.t}.${rawBody}`).digest('hex');
|
|
463
|
+
const valid = crypto.timingSafeEqual(Buffer.from(parts.v1, 'hex'), Buffer.from(expected, 'hex'));
|
|
464
|
+
const recent = Date.now() / 1000 - Number(parts.t) < 300;
|
|
465
|
+
return valid && recent;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
app.post('/webhooks/blind-agents', async (req, res) => {
|
|
469
|
+
const sig = req.headers['x-blindagents-signature'] as string;
|
|
470
|
+
if (!verify(req.body.toString(), sig)) return res.sendStatus(401);
|
|
471
|
+
|
|
472
|
+
const { event, data } = JSON.parse(req.body.toString());
|
|
473
|
+
|
|
474
|
+
const messages: Record<string, string> = {
|
|
475
|
+
'ticket.created': `🎫 *New ticket:* <${data.page_url}|${data.title}> — ${data.priority} priority`,
|
|
476
|
+
'ticket.status_changed': `🔄 *Ticket updated:* ${data.title} → ${data.status}`,
|
|
477
|
+
'ticket.resolved': `✅ *Ticket resolved:* ${data.title}`,
|
|
478
|
+
'contact.created': `👤 *New contact:* ${data.name} (${data.email})`,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
await fetch(SLACK_URL!, {
|
|
482
|
+
method: 'POST',
|
|
483
|
+
headers: { 'Content-Type': 'application/json' },
|
|
484
|
+
body: JSON.stringify({ text: messages[event] ?? `📡 Blind Agents event: ${event}` }),
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
res.sendStatus(200);
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### 4. Rich Slack messages with Block Kit (optional)
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
const block = {
|
|
495
|
+
blocks: [
|
|
496
|
+
{ type: 'header', text: { type: 'plain_text', text: '🎫 New Bug Report' } },
|
|
497
|
+
{
|
|
498
|
+
type: 'section',
|
|
499
|
+
fields: [
|
|
500
|
+
{ type: 'mrkdwn', text: `*Title:*\n${data.title}` },
|
|
501
|
+
{ type: 'mrkdwn', text: `*Priority:*\n${data.priority}` },
|
|
502
|
+
{ type: 'mrkdwn', text: `*Reporter:*\n${data.contact?.name ?? 'Anonymous'}` },
|
|
503
|
+
{ type: 'mrkdwn', text: `*Page:*\n${data.page_url ?? '—'}` },
|
|
504
|
+
],
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
type: 'actions',
|
|
508
|
+
elements: [{
|
|
509
|
+
type: 'button',
|
|
510
|
+
text: { type: 'plain_text', text: 'View ticket' },
|
|
511
|
+
url: `https://app.blindagents.com/tickets/${data.id}`,
|
|
512
|
+
style: 'primary',
|
|
513
|
+
}],
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
};
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## Webhooks — n8n integration
|
|
522
|
+
|
|
523
|
+
Connect Blind Agents to any service (Slack, Jira, Notion, Google Sheets, HubSpot…) without writing a server.
|
|
524
|
+
|
|
525
|
+
### 1. Add a Webhook trigger node in n8n
|
|
526
|
+
|
|
527
|
+
1. Create a new workflow in n8n
|
|
528
|
+
2. Add a **Webhook** node → Method: **POST** → copy the URL
|
|
529
|
+
3. In **Blind Agents → Webhooks → Add webhook**, paste that URL and select events
|
|
530
|
+
4. Copy the **signing secret**
|
|
531
|
+
|
|
532
|
+
### 2. Verify the signature (Code node)
|
|
533
|
+
|
|
534
|
+
Add a **Code** node after the Webhook trigger. Store the secret in **n8n Credentials** as `BLIND_AGENTS_SECRET`:
|
|
535
|
+
|
|
536
|
+
```js
|
|
537
|
+
const crypto = require('crypto');
|
|
538
|
+
|
|
539
|
+
const secret = $env.BLIND_AGENTS_SECRET;
|
|
540
|
+
const signature = $input.first().headers['x-blindagents-signature'];
|
|
541
|
+
const rawBody = JSON.stringify($input.first().body);
|
|
542
|
+
|
|
543
|
+
const parts = Object.fromEntries(signature.split(',').map(p => p.split('=')));
|
|
544
|
+
const hmac = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
|
|
545
|
+
|
|
546
|
+
const isValid = crypto.timingSafeEqual(Buffer.from(parts.v1, 'hex'), Buffer.from(hmac, 'hex'));
|
|
547
|
+
const isRecent = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
|
|
548
|
+
|
|
549
|
+
if (!isValid || !isRecent) throw new Error('Invalid signature — aborting workflow');
|
|
550
|
+
|
|
551
|
+
return $input.all();
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### 3. Send to Slack (Slack node)
|
|
555
|
+
|
|
556
|
+
Add a **Slack** node, connect it to your workspace, and set the message text using expressions:
|
|
557
|
+
|
|
558
|
+
```
|
|
559
|
+
🎫 New ticket: {{ $json.body.data.title }}
|
|
560
|
+
Priority: {{ $json.body.data.priority }}
|
|
561
|
+
Reporter: {{ $json.body.data.contact.name }}
|
|
562
|
+
Page: {{ $json.body.data.page_url }}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### 4. Always respond with 200
|
|
566
|
+
|
|
567
|
+
Add a **Respond to Webhook** node at the end — Response Code `200`. Without it, Blind Agents marks the delivery as failed and retries.
|
|
568
|
+
|
|
569
|
+
### Other n8n workflow ideas
|
|
570
|
+
|
|
571
|
+
| Trigger | Action |
|
|
572
|
+
|---|---|
|
|
573
|
+
| `ticket.created` | Create Jira / Linear issue |
|
|
574
|
+
| `ticket.created` (high priority) | Send Gmail alert to on-call |
|
|
575
|
+
| `ticket.resolved` | Append row to Google Sheet |
|
|
576
|
+
| `contact.created` | Sync contact to HubSpot / Mailchimp |
|
|
577
|
+
| `ticket.status_changed` | Update Notion database row |
|
|
578
|
+
| Any event | POST to Zapier catch hook for further routing |
|
|
579
|
+
|
|
580
|
+
### Self-hosting n8n with Docker
|
|
581
|
+
|
|
582
|
+
```bash
|
|
583
|
+
docker run -it --rm \
|
|
584
|
+
--name n8n \
|
|
585
|
+
-p 5678:5678 \
|
|
586
|
+
-e N8N_BASIC_AUTH_ACTIVE=true \
|
|
587
|
+
-e N8N_BASIC_AUTH_USER=admin \
|
|
588
|
+
-e N8N_BASIC_AUTH_PASSWORD=yourpassword \
|
|
589
|
+
-v ~/.n8n:/home/node/.n8n \
|
|
590
|
+
docker.n8n.io/n8nio/n8n
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Expose port 5678 via your domain and use `https://your-domain.com/webhook/...` as the Blind Agents webhook URL.
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## Webhook payload reference
|
|
598
|
+
|
|
599
|
+
Every event sends this JSON body:
|
|
600
|
+
|
|
601
|
+
```json
|
|
602
|
+
{
|
|
603
|
+
"id": "evt_01HXYZ...",
|
|
604
|
+
"event": "ticket.created",
|
|
605
|
+
"created_at": "2026-04-09T03:00:00Z",
|
|
606
|
+
"data": {
|
|
607
|
+
"id": "tkt_01HXYZ...",
|
|
608
|
+
"title": "Button not working on checkout",
|
|
609
|
+
"status": "open",
|
|
610
|
+
"priority": "high",
|
|
611
|
+
"type": "bug",
|
|
612
|
+
"contact": { "name": "Jane Doe", "email": "jane@example.com" },
|
|
613
|
+
"page_url": "https://yoursite.com/checkout"
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### Supported events
|
|
619
|
+
|
|
620
|
+
| Event | Fired when |
|
|
621
|
+
|---|---|
|
|
622
|
+
| `ticket.created` | A new ticket is submitted |
|
|
623
|
+
| `ticket.status_changed` | Ticket status is updated |
|
|
624
|
+
| `ticket.resolved` | Ticket is marked resolved |
|
|
625
|
+
| `ticket.closed` | Ticket is closed |
|
|
626
|
+
| `contact.created` | A new contact is registered |
|
|
627
|
+
| `conversation.created` | A new chat conversation starts |
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## Signature verification
|
|
632
|
+
|
|
633
|
+
Every request includes an `X-BlindAgents-Signature` header:
|
|
634
|
+
|
|
635
|
+
```
|
|
636
|
+
t={unix_timestamp},v1={hmac_sha256(secret, "{timestamp}.{raw_body}")}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
**Node.js:**
|
|
640
|
+
```ts
|
|
641
|
+
import crypto from 'crypto';
|
|
642
|
+
|
|
643
|
+
function verifyWebhook(rawBody: string, signature: string, secret: string) {
|
|
644
|
+
const parts = Object.fromEntries(signature.split(',').map(p => p.split('=')));
|
|
645
|
+
const expected = crypto
|
|
646
|
+
.createHmac('sha256', secret)
|
|
647
|
+
.update(`${parts.t}.${rawBody}`)
|
|
648
|
+
.digest('hex');
|
|
649
|
+
const valid = crypto.timingSafeEqual(Buffer.from(parts.v1, 'hex'), Buffer.from(expected, 'hex'));
|
|
650
|
+
const recent = Date.now() / 1000 - Number(parts.t) < 300;
|
|
651
|
+
return valid && recent;
|
|
95
652
|
}
|
|
96
653
|
```
|
|
97
654
|
|
|
98
|
-
|
|
655
|
+
**Python:**
|
|
656
|
+
```python
|
|
657
|
+
import hmac, hashlib, time
|
|
658
|
+
|
|
659
|
+
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
|
|
660
|
+
parts = dict(p.split("=", 1) for p in signature.split(","))
|
|
661
|
+
msg = f"{parts['t']}.".encode() + raw_body
|
|
662
|
+
expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
|
|
663
|
+
return (
|
|
664
|
+
hmac.compare_digest(parts["v1"], expected)
|
|
665
|
+
and abs(time.time() - int(parts["t"])) < 300
|
|
666
|
+
)
|
|
667
|
+
```
|
|
99
668
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
| `src` | `string` | `"https://cdn.blindagents.com/report.js"` | Override CDN URL (self-hosting) |
|
|
112
|
-
| `onLoad` | `() => void` | — | Called when the script loads successfully |
|
|
113
|
-
| `onError` | `(error: Error) => void` | — | Called if the script fails to load |
|
|
669
|
+
**Go:**
|
|
670
|
+
```go
|
|
671
|
+
import (
|
|
672
|
+
"crypto/hmac"
|
|
673
|
+
"crypto/sha256"
|
|
674
|
+
"encoding/hex"
|
|
675
|
+
"math"
|
|
676
|
+
"strings"
|
|
677
|
+
"strconv"
|
|
678
|
+
"time"
|
|
679
|
+
)
|
|
114
680
|
|
|
115
|
-
|
|
681
|
+
func VerifyWebhook(rawBody []byte, signature, secret string) bool {
|
|
682
|
+
parts := map[string]string{}
|
|
683
|
+
for _, p := range strings.Split(signature, ",") {
|
|
684
|
+
kv := strings.SplitN(p, "=", 2)
|
|
685
|
+
if len(kv) == 2 { parts[kv[0]] = kv[1] }
|
|
686
|
+
}
|
|
687
|
+
ts, _ := strconv.ParseInt(parts["t"], 10, 64)
|
|
688
|
+
msg := append([]byte(parts["t"]+"."), rawBody...)
|
|
689
|
+
mac := hmac.New(sha256.New, []byte(secret))
|
|
690
|
+
mac.Write(msg)
|
|
691
|
+
expected, _ := hex.DecodeString(hex.EncodeToString(mac.Sum(nil)))
|
|
692
|
+
got, _ := hex.DecodeString(parts["v1"])
|
|
693
|
+
return hmac.Equal(got, expected) && math.Abs(float64(time.Now().Unix()-ts)) < 300
|
|
694
|
+
}
|
|
695
|
+
```
|
|
116
696
|
|
|
117
|
-
- `
|
|
118
|
-
- `react-blind-agents/next` — Next.js only. Uses `next/script` for correct hydration, strategy support, and deduplication across navigations.
|
|
697
|
+
> Always use constant-time comparison (`timingSafeEqual` / `hmac.Equal` / `hmac.compare_digest`) and reject requests older than 5 minutes to prevent replay attacks.
|
|
119
698
|
|
|
120
|
-
|
|
699
|
+
---
|
|
121
700
|
|
|
122
701
|
## License
|
|
123
702
|
|
|
124
|
-
MIT
|
|
703
|
+
MIT © [Blind Agents](https://blindagents.com)
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duvandroid/react-blind-agents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "React component for the Blind Agents pixel widget — bug reporter, webchat, and product guides.",
|
|
5
5
|
"author": "Blind Agents <hola@blindagents.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"keywords": [
|
|
7
|
+
"keywords": [
|
|
8
|
+
"react",
|
|
9
|
+
"blind-agents",
|
|
10
|
+
"widget",
|
|
11
|
+
"bug-reporter",
|
|
12
|
+
"webchat",
|
|
13
|
+
"next"
|
|
14
|
+
],
|
|
8
15
|
"main": "./dist/index.cjs",
|
|
9
16
|
"module": "./dist/index.js",
|
|
10
17
|
"types": "./dist/index.d.ts",
|