feedoback-react 0.1.0
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 +125 -0
- package/dist/index.cjs +144 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +160 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.js +138 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Feedoback
|
|
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
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# feedoback-react
|
|
2
|
+
|
|
3
|
+
React and Next.js SDK for the [Feedoback](https://feedoback.com) website feedback and
|
|
4
|
+
support widget. Drop one component in and visitors can point at an element and comment,
|
|
5
|
+
record their screen, send feedback, or open a support thread — no manual script tags, no
|
|
6
|
+
wrapper components to hand-write.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install feedoback-react
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
### Next.js (App Router)
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
// app/layout.tsx
|
|
18
|
+
import { FeedbackWidget } from "feedoback-react";
|
|
19
|
+
|
|
20
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
21
|
+
return (
|
|
22
|
+
<html lang="en">
|
|
23
|
+
<body>
|
|
24
|
+
{children}
|
|
25
|
+
<FeedbackWidget projectKey="pk_live_your_key" />
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`FeedbackWidget` is a client component; you can render it straight from a server layout.
|
|
33
|
+
|
|
34
|
+
### React (Vite, CRA, anything)
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// src/App.tsx
|
|
38
|
+
import { FeedbackWidget } from "feedoback-react";
|
|
39
|
+
|
|
40
|
+
export default function App() {
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<YourApp />
|
|
44
|
+
<FeedbackWidget projectKey="pk_live_your_key" />
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Your public key is on the project's **Install** page in the dashboard. It identifies the
|
|
51
|
+
project on a page — it is public, not a secret.
|
|
52
|
+
|
|
53
|
+
## Identify who is signed in
|
|
54
|
+
|
|
55
|
+
Pass the current user; the widget shows who left each message so you can reply. Every field
|
|
56
|
+
is optional.
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<FeedbackWidget
|
|
60
|
+
projectKey="pk_live_your_key"
|
|
61
|
+
visitor={user ? { id: user.id, email: user.email, name: user.name } : undefined}
|
|
62
|
+
/>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If your project is rolled out to **listed visitors**, this is also how the widget knows
|
|
66
|
+
whether to appear at all.
|
|
67
|
+
|
|
68
|
+
## Drive it from your own UI
|
|
69
|
+
|
|
70
|
+
`useFeedback()` returns the imperative API from anywhere in the tree — no provider needed.
|
|
71
|
+
Calls made before the widget finishes loading are queued and replayed.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { useFeedback } from "feedoback-react";
|
|
75
|
+
|
|
76
|
+
function HelpMenu() {
|
|
77
|
+
const feedback = useFeedback();
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
<button onClick={feedback.startFeedback}>Report a bug</button>
|
|
81
|
+
<button onClick={feedback.startRecording}>Record your screen</button>
|
|
82
|
+
<button onClick={feedback.openSupport}>Contact support</button>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| Method | Effect |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `identify(visitor)` | Tell the widget who is signed in |
|
|
91
|
+
| `setContext(context)` | Attach flat scalar context (plan, route…) to new threads |
|
|
92
|
+
| `open()` / `close()` / `toggle()` | Control the panel |
|
|
93
|
+
| `startFeedback()` | Start element-pointing feedback |
|
|
94
|
+
| `startRecording()` | Open straight into screen recording |
|
|
95
|
+
| `openSupport()` | Open straight into a support thread |
|
|
96
|
+
| `destroy()` | Remove the widget (call on sign-out in an SPA) |
|
|
97
|
+
|
|
98
|
+
## Props
|
|
99
|
+
|
|
100
|
+
| Prop | Type | Default | Notes |
|
|
101
|
+
| --- | --- | --- | --- |
|
|
102
|
+
| `projectKey` | `string` | — | Required. `pk_…` from the dashboard |
|
|
103
|
+
| `host` | `string` | `https://feedoback.com` | Origin serving `widget.js` |
|
|
104
|
+
| `version` | `string` | — | Your release, stored with every thread |
|
|
105
|
+
| `scriptSrc` | `string` | — | Full override of the script URL (self-hosting) |
|
|
106
|
+
| `visitor` | `{ id?, email?, name? }` | — | Identify on load and on change |
|
|
107
|
+
| `context` | `Record<string, string \| number \| boolean \| null>` | — | Custom context |
|
|
108
|
+
| `destroyOnUnmount` | `boolean` | `false` | Tear down when the component unmounts |
|
|
109
|
+
| `onLoad` / `onError` | `() => void` | — | Script load callbacks |
|
|
110
|
+
|
|
111
|
+
## Content-Security-Policy
|
|
112
|
+
|
|
113
|
+
If your site sends a CSP, allow your Feedoback host in `script-src` and `connect-src`, and
|
|
114
|
+
add `data:` and `blob:` to `img-src`. The widget needs no `'unsafe-inline'`.
|
|
115
|
+
|
|
116
|
+
## How it works
|
|
117
|
+
|
|
118
|
+
The package is a thin, dependency-free loader: it injects the hosted `widget.js` (with the
|
|
119
|
+
attributes it reads from its own tag) exactly once, and proxies `useFeedback()` calls to the
|
|
120
|
+
widget's global — queueing anything called before it boots. The widget itself is served and
|
|
121
|
+
versioned by Feedoback, so it updates without you shipping a new build.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
// src/FeedbackWidget.tsx
|
|
8
|
+
|
|
9
|
+
// src/core.ts
|
|
10
|
+
var DEFAULT_HOST = "https://feedoback.com";
|
|
11
|
+
var GLOBAL = "Feedoback";
|
|
12
|
+
var SDK_ATTRIBUTE = "data-feedoback-sdk";
|
|
13
|
+
function ensureGlobal(win) {
|
|
14
|
+
return win[GLOBAL] ?? (win[GLOBAL] = { q: [] });
|
|
15
|
+
}
|
|
16
|
+
function dispatch(win, method, arg) {
|
|
17
|
+
const widget = ensureGlobal(win);
|
|
18
|
+
const handler = widget[method];
|
|
19
|
+
if (typeof handler === "function") {
|
|
20
|
+
handler(arg);
|
|
21
|
+
} else {
|
|
22
|
+
(widget.q ?? (widget.q = [])).push([method, arg]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function scriptUrl({ host, scriptSrc }) {
|
|
26
|
+
if (scriptSrc) return scriptSrc;
|
|
27
|
+
const base = (host ?? DEFAULT_HOST).replace(/\/+$/, "");
|
|
28
|
+
return `${base}/widget.js`;
|
|
29
|
+
}
|
|
30
|
+
function existingScript(doc, projectKey) {
|
|
31
|
+
const scripts = doc.querySelectorAll(`script[${SDK_ATTRIBUTE}]`);
|
|
32
|
+
for (const script of scripts) {
|
|
33
|
+
if (script.getAttribute("data-project") === projectKey) return script;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function loadWidget(win, options) {
|
|
38
|
+
const doc = win.document;
|
|
39
|
+
if (!doc) return null;
|
|
40
|
+
ensureGlobal(win);
|
|
41
|
+
const already = existingScript(doc, options.projectKey);
|
|
42
|
+
if (already) return already;
|
|
43
|
+
const script = doc.createElement("script");
|
|
44
|
+
script.src = scriptUrl(options);
|
|
45
|
+
script.async = true;
|
|
46
|
+
script.setAttribute("data-project", options.projectKey);
|
|
47
|
+
if (options.version) script.setAttribute("data-version", options.version);
|
|
48
|
+
script.setAttribute(SDK_ATTRIBUTE, "");
|
|
49
|
+
if (options.onLoad) script.addEventListener("load", options.onLoad, { once: true });
|
|
50
|
+
if (options.onError) script.addEventListener("error", options.onError, { once: true });
|
|
51
|
+
(doc.body ?? doc.head ?? doc.documentElement).appendChild(script);
|
|
52
|
+
return script;
|
|
53
|
+
}
|
|
54
|
+
function unloadWidget(win, script) {
|
|
55
|
+
win[GLOBAL]?.destroy?.();
|
|
56
|
+
script?.remove();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/FeedbackWidget.tsx
|
|
60
|
+
function FeedbackWidget({
|
|
61
|
+
projectKey,
|
|
62
|
+
host,
|
|
63
|
+
version,
|
|
64
|
+
scriptSrc,
|
|
65
|
+
visitor,
|
|
66
|
+
context,
|
|
67
|
+
destroyOnUnmount = false,
|
|
68
|
+
onLoad,
|
|
69
|
+
onError
|
|
70
|
+
}) {
|
|
71
|
+
const scriptRef = react.useRef(null);
|
|
72
|
+
const onLoadRef = react.useRef(onLoad);
|
|
73
|
+
const onErrorRef = react.useRef(onError);
|
|
74
|
+
onLoadRef.current = onLoad;
|
|
75
|
+
onErrorRef.current = onError;
|
|
76
|
+
react.useEffect(() => {
|
|
77
|
+
if (typeof window === "undefined") return;
|
|
78
|
+
scriptRef.current = loadWidget(window, {
|
|
79
|
+
projectKey,
|
|
80
|
+
host,
|
|
81
|
+
version,
|
|
82
|
+
scriptSrc,
|
|
83
|
+
onLoad: () => onLoadRef.current?.(),
|
|
84
|
+
onError: () => onErrorRef.current?.()
|
|
85
|
+
});
|
|
86
|
+
return () => {
|
|
87
|
+
if (destroyOnUnmount) {
|
|
88
|
+
unloadWidget(window, scriptRef.current);
|
|
89
|
+
scriptRef.current = null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}, [projectKey, host, version, scriptSrc, destroyOnUnmount]);
|
|
93
|
+
react.useEffect(() => {
|
|
94
|
+
if (typeof window === "undefined" || !visitor) return;
|
|
95
|
+
dispatch(window, "identify", visitor);
|
|
96
|
+
}, [visitor?.id, visitor?.email, visitor?.name]);
|
|
97
|
+
const contextKey = context ? JSON.stringify(context) : null;
|
|
98
|
+
react.useEffect(() => {
|
|
99
|
+
if (typeof window === "undefined" || !context) return;
|
|
100
|
+
dispatch(window, "setContext", context);
|
|
101
|
+
}, [contextKey]);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function FeedbackProvider({ children, ...props }) {
|
|
105
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
106
|
+
children,
|
|
107
|
+
/* @__PURE__ */ jsxRuntime.jsx(FeedbackWidget, { ...props })
|
|
108
|
+
] });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/api.ts
|
|
112
|
+
function resolveWindow() {
|
|
113
|
+
return typeof window !== "undefined" ? window : void 0;
|
|
114
|
+
}
|
|
115
|
+
function createFeedbackApi(getWindow = resolveWindow) {
|
|
116
|
+
const send = (method, arg) => {
|
|
117
|
+
const win = getWindow();
|
|
118
|
+
if (win) dispatch(win, method, arg);
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
identify: (visitor) => send("identify", visitor),
|
|
122
|
+
setContext: (context) => send("setContext", context),
|
|
123
|
+
open: () => send("open"),
|
|
124
|
+
close: () => send("close"),
|
|
125
|
+
toggle: () => send("toggle"),
|
|
126
|
+
startFeedback: () => send("startFeedback"),
|
|
127
|
+
startRecording: () => send("startRecording"),
|
|
128
|
+
openSupport: () => send("openSupport"),
|
|
129
|
+
destroy: () => send("destroy")
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/useFeedback.ts
|
|
134
|
+
function useFeedback() {
|
|
135
|
+
return react.useMemo(() => createFeedbackApi(), []);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
exports.DEFAULT_HOST = DEFAULT_HOST;
|
|
139
|
+
exports.FeedbackProvider = FeedbackProvider;
|
|
140
|
+
exports.FeedbackWidget = FeedbackWidget;
|
|
141
|
+
exports.createFeedbackApi = createFeedbackApi;
|
|
142
|
+
exports.useFeedback = useFeedback;
|
|
143
|
+
//# sourceMappingURL=index.cjs.map
|
|
144
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core.ts","../src/FeedbackWidget.tsx","../src/FeedbackProvider.tsx","../src/api.ts","../src/useFeedback.ts"],"names":["useRef","useEffect","jsxs","Fragment","jsx","useMemo"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * The framework-agnostic core.\n *\n * Everything that touches `window` or the DOM lives here, behind plain\n * functions that take the target `Window` explicitly. That is what makes the\n * behaviour testable without a browser and keeps the React layer a thin shell\n * over it. Nothing here imports React.\n */\nimport type { FeedobackApi, FeedobackVisitor, FeedobackContext } from \"./types\";\n\nexport const DEFAULT_HOST = \"https://feedoback.com\";\n\n/** The global the hosted widget installs. Keep this name in step with widget.js. */\nconst GLOBAL = \"Feedoback\" as const;\n\n/** Our marker attribute, so we recognise a script we injected. */\nconst SDK_ATTRIBUTE = \"data-feedoback-sdk\";\n\n/** A call queued before the widget booted: `[method, arg?]`, replayed on boot. */\ntype QueuedCall = [keyof FeedobackApi, unknown?];\n\n/**\n * The shape `window.Feedoback` can take: a bare queue stub before the script\n * loads, or the full API afterwards. Every method is optional because at any\n * instant only one of those two states is present.\n */\ntype WidgetGlobal = Partial<FeedobackApi> & { q?: QueuedCall[] };\n\nexport type WidgetWindow = Window & { [GLOBAL]?: WidgetGlobal };\n\n/**\n * Ensures the queue stub exists without clobbering a real API the widget may\n * have already installed. `??=` leaves an existing value untouched, so calling\n * this repeatedly is safe.\n */\nfunction ensureGlobal(win: WidgetWindow): WidgetGlobal {\n return (win[GLOBAL] ??= { q: [] });\n}\n\n/**\n * Forwards a call to the widget, or queues it if the widget has not booted.\n *\n * This is the same queue-or-call pattern the hosted widget replays on boot, so\n * a call made a millisecond before `widget.js` finishes loading behaves\n * identically to one made a second after.\n */\nexport function dispatch(win: WidgetWindow, method: keyof FeedobackApi, arg?: unknown): void {\n const widget = ensureGlobal(win);\n const handler = widget[method];\n if (typeof handler === \"function\") {\n (handler as (value?: unknown) => void)(arg);\n } else {\n (widget.q ??= []).push([method, arg]);\n }\n}\n\nexport type LoadOptions = {\n projectKey: string;\n host?: string;\n version?: string;\n scriptSrc?: string;\n onLoad?: () => void;\n onError?: () => void;\n};\n\nfunction scriptUrl({ host, scriptSrc }: LoadOptions): string {\n if (scriptSrc) return scriptSrc;\n const base = (host ?? DEFAULT_HOST).replace(/\\/+$/, \"\");\n return `${base}/widget.js`;\n}\n\n/** Finds a script this SDK already injected for the same project, if any. */\nfunction existingScript(doc: Document, projectKey: string): HTMLScriptElement | null {\n const scripts = doc.querySelectorAll<HTMLScriptElement>(`script[${SDK_ATTRIBUTE}]`);\n for (const script of scripts) {\n if (script.getAttribute(\"data-project\") === projectKey) return script;\n }\n return null;\n}\n\n/**\n * Injects `widget.js` with the attributes the widget reads from its own tag\n * (`data-project`, optional `data-version`). Idempotent: a second call for the\n * same project returns the existing tag rather than stacking a duplicate, which\n * matters under React Strict Mode and single-page navigation.\n *\n * The queue stub is created first, so any identity queued in the same tick is\n * in place before the async script evaluates and replays it.\n */\nexport function loadWidget(win: WidgetWindow, options: LoadOptions): HTMLScriptElement | null {\n const doc = win.document;\n if (!doc) return null;\n\n ensureGlobal(win);\n\n const already = existingScript(doc, options.projectKey);\n if (already) return already;\n\n const script = doc.createElement(\"script\");\n script.src = scriptUrl(options);\n script.async = true;\n script.setAttribute(\"data-project\", options.projectKey);\n if (options.version) script.setAttribute(\"data-version\", options.version);\n script.setAttribute(SDK_ATTRIBUTE, \"\");\n if (options.onLoad) script.addEventListener(\"load\", options.onLoad, { once: true });\n if (options.onError) script.addEventListener(\"error\", options.onError, { once: true });\n\n (doc.body ?? doc.head ?? doc.documentElement).appendChild(script);\n return script;\n}\n\n/**\n * Removes the widget and the script tag this SDK injected. Uses the widget's\n * own `destroy()` when present so it can tear down its shadow hosts and the\n * global; then drops our tag so a later mount starts clean.\n */\nexport function unloadWidget(win: WidgetWindow, script: HTMLScriptElement | null): void {\n win[GLOBAL]?.destroy?.();\n script?.remove();\n}\n","\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { dispatch, loadWidget, unloadWidget } from \"./core\";\nimport type { FeedbackWidgetProps } from \"./types\";\n\n/**\n * Loads the Feedoback widget on the page. Render it once, near the root of the\n * app; it draws nothing itself and returns `null`. In Next.js App Router it is\n * a client component, so it can live in a server layout directly.\n *\n * ```tsx\n * <FeedbackWidget projectKey=\"pk_live_123\" />\n * ```\n */\nexport function FeedbackWidget({\n projectKey,\n host,\n version,\n scriptSrc,\n visitor,\n context,\n destroyOnUnmount = false,\n onLoad,\n onError,\n}: FeedbackWidgetProps): null {\n const scriptRef = useRef<HTMLScriptElement | null>(null);\n\n // Inject the script. Re-runs only when what the tag encodes changes; the\n // load is idempotent, so Strict Mode's double mount does not stack a second\n // launcher. onLoad/onError are read through refs so a new closure each render\n // does not retrigger the injection.\n const onLoadRef = useRef(onLoad);\n const onErrorRef = useRef(onError);\n onLoadRef.current = onLoad;\n onErrorRef.current = onError;\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n scriptRef.current = loadWidget(window, {\n projectKey,\n host,\n version,\n scriptSrc,\n onLoad: () => onLoadRef.current?.(),\n onError: () => onErrorRef.current?.(),\n });\n return () => {\n if (destroyOnUnmount) {\n unloadWidget(window, scriptRef.current);\n scriptRef.current = null;\n }\n };\n }, [projectKey, host, version, scriptSrc, destroyOnUnmount]);\n\n // Identity is kept in its own effect so it re-applies when the signed-in user\n // changes without reloading the script. Queued before boot, forwarded after.\n useEffect(() => {\n if (typeof window === \"undefined\" || !visitor) return;\n dispatch(window, \"identify\", visitor);\n }, [visitor?.id, visitor?.email, visitor?.name]);\n\n const contextKey = context ? JSON.stringify(context) : null;\n useEffect(() => {\n if (typeof window === \"undefined\" || !context) return;\n dispatch(window, \"setContext\", context);\n // context is compared by value through contextKey; re-reading the object\n // here is intentional and matches that comparison.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [contextKey]);\n\n return null;\n}\n","\"use client\";\n\nimport type { PropsWithChildren } from \"react\";\n\nimport { FeedbackWidget } from \"./FeedbackWidget\";\nimport type { FeedbackWidgetProps } from \"./types\";\n\n/**\n * Convenience wrapper for teams who prefer wrapping the app over dropping a\n * component in a layout. It renders its children and loads the widget beside\n * them; there is no context to consume, because {@link useFeedback} talks to\n * the widget's global directly and works anywhere below (or beside) this.\n *\n * ```tsx\n * <FeedbackProvider projectKey=\"pk_live_123\" visitor={user}>\n * <App />\n * </FeedbackProvider>\n * ```\n */\nexport function FeedbackProvider({ children, ...props }: PropsWithChildren<FeedbackWidgetProps>) {\n return (\n <>\n {children}\n <FeedbackWidget {...props} />\n </>\n );\n}\n","/**\n * Builds the imperative {@link FeedobackApi}. Each method is a thin proxy over\n * {@link dispatch}, so the same object works before and after the widget loads\n * and is safe to hold onto across renders.\n *\n * The window is resolved lazily through `getWindow` rather than captured, which\n * keeps the API defined on the server (where there is no window) and lets it\n * start working the moment one exists.\n */\nimport { dispatch, type WidgetWindow } from \"./core\";\nimport type { FeedobackApi } from \"./types\";\n\nfunction resolveWindow(): WidgetWindow | undefined {\n return typeof window !== \"undefined\" ? (window as WidgetWindow) : undefined;\n}\n\nexport function createFeedbackApi(getWindow: () => WidgetWindow | undefined = resolveWindow): FeedobackApi {\n const send = (method: keyof FeedobackApi, arg?: unknown) => {\n const win = getWindow();\n if (win) dispatch(win, method, arg);\n };\n\n return {\n identify: (visitor) => send(\"identify\", visitor),\n setContext: (context) => send(\"setContext\", context),\n open: () => send(\"open\"),\n close: () => send(\"close\"),\n toggle: () => send(\"toggle\"),\n startFeedback: () => send(\"startFeedback\"),\n startRecording: () => send(\"startRecording\"),\n openSupport: () => send(\"openSupport\"),\n destroy: () => send(\"destroy\"),\n };\n}\n","\"use client\";\n\nimport { useMemo } from \"react\";\n\nimport { createFeedbackApi } from \"./api\";\nimport type { FeedobackApi } from \"./types\";\n\n/**\n * Returns the imperative widget API: `identify`, `open`, `startFeedback`,\n * `destroy` and the rest. The object is stable across renders and works from\n * anywhere the widget is loaded — no provider required, because it proxies to\n * the widget's global. Calls made before the widget finishes loading are\n * queued and replayed.\n *\n * ```tsx\n * const feedback = useFeedback();\n * <button onClick={feedback.startFeedback}>Report a bug</button>\n * ```\n */\nexport function useFeedback(): FeedobackApi {\n return useMemo(() => createFeedbackApi(), []);\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { PropsWithChildren } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public types for the Feedoback React SDK.
|
|
6
|
+
*
|
|
7
|
+
* These mirror the contract of the hosted `widget.js`: an identity is at most
|
|
8
|
+
* an id, an email and a name, and custom context is a flat map of scalars.
|
|
9
|
+
* Everything the customer passes is their own claim about who someone is — the
|
|
10
|
+
* server never treats it as authoritative — so the shapes stay deliberately
|
|
11
|
+
* small.
|
|
12
|
+
*/
|
|
13
|
+
/** Who the current visitor is. Every field is optional; all are the page's claim. */
|
|
14
|
+
type FeedobackVisitor = {
|
|
15
|
+
id?: string;
|
|
16
|
+
email?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Flat, scalar-only context stored alongside a thread (plan, route, tier…). */
|
|
20
|
+
type FeedobackContext = Record<string, string | number | boolean | null>;
|
|
21
|
+
/**
|
|
22
|
+
* The imperative surface returned by {@link useFeedback}. It proxies to the
|
|
23
|
+
* hosted widget on `window.Feedoback`, queueing calls made before the script
|
|
24
|
+
* has finished loading and forwarding them once it has.
|
|
25
|
+
*/
|
|
26
|
+
type FeedobackApi = {
|
|
27
|
+
/** Tell the widget who is signed in. Safe before the script has loaded. */
|
|
28
|
+
identify: (visitor: FeedobackVisitor) => void;
|
|
29
|
+
/** Attach custom context to threads opened from now on. */
|
|
30
|
+
setContext: (context: FeedobackContext) => void;
|
|
31
|
+
/** Open the panel on its menu. */
|
|
32
|
+
open: () => void;
|
|
33
|
+
/** Close the panel. */
|
|
34
|
+
close: () => void;
|
|
35
|
+
/** Toggle the panel. */
|
|
36
|
+
toggle: () => void;
|
|
37
|
+
/** Start element-pointing feedback (the picker). */
|
|
38
|
+
startFeedback: () => void;
|
|
39
|
+
/** Open the panel straight into screen recording. */
|
|
40
|
+
startRecording: () => void;
|
|
41
|
+
/** Open the panel straight into a support thread. */
|
|
42
|
+
openSupport: () => void;
|
|
43
|
+
/** Remove the widget from the page. Call on sign-out in a single-page app. */
|
|
44
|
+
destroy: () => void;
|
|
45
|
+
};
|
|
46
|
+
/** Configuration shared by {@link FeedbackWidget} and {@link FeedbackProvider}. */
|
|
47
|
+
type FeedbackWidgetProps = {
|
|
48
|
+
/**
|
|
49
|
+
* The project's public key, `pk_…`. It identifies the project on a page; it
|
|
50
|
+
* is public, not a secret, and is used exactly as given.
|
|
51
|
+
*/
|
|
52
|
+
projectKey: string;
|
|
53
|
+
/**
|
|
54
|
+
* Origin that serves `widget.js`, without a trailing slash.
|
|
55
|
+
* Defaults to `https://feedoback.com`.
|
|
56
|
+
*/
|
|
57
|
+
host?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Your release string, stored with every thread so feedback is filed under
|
|
60
|
+
* the build it came from. Wire it from your build (e.g. a package version).
|
|
61
|
+
*/
|
|
62
|
+
version?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Full override of the script URL. Takes precedence over {@link host}; use it
|
|
65
|
+
* for self-hosted or proxied deployments.
|
|
66
|
+
*/
|
|
67
|
+
scriptSrc?: string;
|
|
68
|
+
/** Identify this visitor as soon as the widget loads, and whenever it changes. */
|
|
69
|
+
visitor?: FeedobackVisitor;
|
|
70
|
+
/** Attach this context as soon as the widget loads, and whenever it changes. */
|
|
71
|
+
context?: FeedobackContext;
|
|
72
|
+
/**
|
|
73
|
+
* Remove the widget when the component unmounts. Off by default: the widget
|
|
74
|
+
* is normally mounted once for the app's lifetime.
|
|
75
|
+
*/
|
|
76
|
+
destroyOnUnmount?: boolean;
|
|
77
|
+
/** Called once `widget.js` has loaded. */
|
|
78
|
+
onLoad?: () => void;
|
|
79
|
+
/** Called if `widget.js` fails to load (blocked, offline, wrong host). */
|
|
80
|
+
onError?: () => void;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Loads the Feedoback widget on the page. Render it once, near the root of the
|
|
85
|
+
* app; it draws nothing itself and returns `null`. In Next.js App Router it is
|
|
86
|
+
* a client component, so it can live in a server layout directly.
|
|
87
|
+
*
|
|
88
|
+
* ```tsx
|
|
89
|
+
* <FeedbackWidget projectKey="pk_live_123" />
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
declare function FeedbackWidget({ projectKey, host, version, scriptSrc, visitor, context, destroyOnUnmount, onLoad, onError, }: FeedbackWidgetProps): null;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Convenience wrapper for teams who prefer wrapping the app over dropping a
|
|
96
|
+
* component in a layout. It renders its children and loads the widget beside
|
|
97
|
+
* them; there is no context to consume, because {@link useFeedback} talks to
|
|
98
|
+
* the widget's global directly and works anywhere below (or beside) this.
|
|
99
|
+
*
|
|
100
|
+
* ```tsx
|
|
101
|
+
* <FeedbackProvider projectKey="pk_live_123" visitor={user}>
|
|
102
|
+
* <App />
|
|
103
|
+
* </FeedbackProvider>
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
declare function FeedbackProvider({ children, ...props }: PropsWithChildren<FeedbackWidgetProps>): react.JSX.Element;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns the imperative widget API: `identify`, `open`, `startFeedback`,
|
|
110
|
+
* `destroy` and the rest. The object is stable across renders and works from
|
|
111
|
+
* anywhere the widget is loaded — no provider required, because it proxies to
|
|
112
|
+
* the widget's global. Calls made before the widget finishes loading are
|
|
113
|
+
* queued and replayed.
|
|
114
|
+
*
|
|
115
|
+
* ```tsx
|
|
116
|
+
* const feedback = useFeedback();
|
|
117
|
+
* <button onClick={feedback.startFeedback}>Report a bug</button>
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
declare function useFeedback(): FeedobackApi;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The framework-agnostic core.
|
|
124
|
+
*
|
|
125
|
+
* Everything that touches `window` or the DOM lives here, behind plain
|
|
126
|
+
* functions that take the target `Window` explicitly. That is what makes the
|
|
127
|
+
* behaviour testable without a browser and keeps the React layer a thin shell
|
|
128
|
+
* over it. Nothing here imports React.
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
declare const DEFAULT_HOST = "https://feedoback.com";
|
|
132
|
+
/** The global the hosted widget installs. Keep this name in step with widget.js. */
|
|
133
|
+
declare const GLOBAL: "Feedoback";
|
|
134
|
+
/** A call queued before the widget booted: `[method, arg?]`, replayed on boot. */
|
|
135
|
+
type QueuedCall = [keyof FeedobackApi, unknown?];
|
|
136
|
+
/**
|
|
137
|
+
* The shape `window.Feedoback` can take: a bare queue stub before the script
|
|
138
|
+
* loads, or the full API afterwards. Every method is optional because at any
|
|
139
|
+
* instant only one of those two states is present.
|
|
140
|
+
*/
|
|
141
|
+
type WidgetGlobal = Partial<FeedobackApi> & {
|
|
142
|
+
q?: QueuedCall[];
|
|
143
|
+
};
|
|
144
|
+
type WidgetWindow = Window & {
|
|
145
|
+
[GLOBAL]?: WidgetGlobal;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Builds the imperative {@link FeedobackApi}. Each method is a thin proxy over
|
|
150
|
+
* {@link dispatch}, so the same object works before and after the widget loads
|
|
151
|
+
* and is safe to hold onto across renders.
|
|
152
|
+
*
|
|
153
|
+
* The window is resolved lazily through `getWindow` rather than captured, which
|
|
154
|
+
* keeps the API defined on the server (where there is no window) and lets it
|
|
155
|
+
* start working the moment one exists.
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
declare function createFeedbackApi(getWindow?: () => WidgetWindow | undefined): FeedobackApi;
|
|
159
|
+
|
|
160
|
+
export { DEFAULT_HOST, FeedbackProvider, FeedbackWidget, type FeedbackWidgetProps, type FeedobackApi, type FeedobackContext, type FeedobackVisitor, createFeedbackApi, useFeedback };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { PropsWithChildren } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public types for the Feedoback React SDK.
|
|
6
|
+
*
|
|
7
|
+
* These mirror the contract of the hosted `widget.js`: an identity is at most
|
|
8
|
+
* an id, an email and a name, and custom context is a flat map of scalars.
|
|
9
|
+
* Everything the customer passes is their own claim about who someone is — the
|
|
10
|
+
* server never treats it as authoritative — so the shapes stay deliberately
|
|
11
|
+
* small.
|
|
12
|
+
*/
|
|
13
|
+
/** Who the current visitor is. Every field is optional; all are the page's claim. */
|
|
14
|
+
type FeedobackVisitor = {
|
|
15
|
+
id?: string;
|
|
16
|
+
email?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Flat, scalar-only context stored alongside a thread (plan, route, tier…). */
|
|
20
|
+
type FeedobackContext = Record<string, string | number | boolean | null>;
|
|
21
|
+
/**
|
|
22
|
+
* The imperative surface returned by {@link useFeedback}. It proxies to the
|
|
23
|
+
* hosted widget on `window.Feedoback`, queueing calls made before the script
|
|
24
|
+
* has finished loading and forwarding them once it has.
|
|
25
|
+
*/
|
|
26
|
+
type FeedobackApi = {
|
|
27
|
+
/** Tell the widget who is signed in. Safe before the script has loaded. */
|
|
28
|
+
identify: (visitor: FeedobackVisitor) => void;
|
|
29
|
+
/** Attach custom context to threads opened from now on. */
|
|
30
|
+
setContext: (context: FeedobackContext) => void;
|
|
31
|
+
/** Open the panel on its menu. */
|
|
32
|
+
open: () => void;
|
|
33
|
+
/** Close the panel. */
|
|
34
|
+
close: () => void;
|
|
35
|
+
/** Toggle the panel. */
|
|
36
|
+
toggle: () => void;
|
|
37
|
+
/** Start element-pointing feedback (the picker). */
|
|
38
|
+
startFeedback: () => void;
|
|
39
|
+
/** Open the panel straight into screen recording. */
|
|
40
|
+
startRecording: () => void;
|
|
41
|
+
/** Open the panel straight into a support thread. */
|
|
42
|
+
openSupport: () => void;
|
|
43
|
+
/** Remove the widget from the page. Call on sign-out in a single-page app. */
|
|
44
|
+
destroy: () => void;
|
|
45
|
+
};
|
|
46
|
+
/** Configuration shared by {@link FeedbackWidget} and {@link FeedbackProvider}. */
|
|
47
|
+
type FeedbackWidgetProps = {
|
|
48
|
+
/**
|
|
49
|
+
* The project's public key, `pk_…`. It identifies the project on a page; it
|
|
50
|
+
* is public, not a secret, and is used exactly as given.
|
|
51
|
+
*/
|
|
52
|
+
projectKey: string;
|
|
53
|
+
/**
|
|
54
|
+
* Origin that serves `widget.js`, without a trailing slash.
|
|
55
|
+
* Defaults to `https://feedoback.com`.
|
|
56
|
+
*/
|
|
57
|
+
host?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Your release string, stored with every thread so feedback is filed under
|
|
60
|
+
* the build it came from. Wire it from your build (e.g. a package version).
|
|
61
|
+
*/
|
|
62
|
+
version?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Full override of the script URL. Takes precedence over {@link host}; use it
|
|
65
|
+
* for self-hosted or proxied deployments.
|
|
66
|
+
*/
|
|
67
|
+
scriptSrc?: string;
|
|
68
|
+
/** Identify this visitor as soon as the widget loads, and whenever it changes. */
|
|
69
|
+
visitor?: FeedobackVisitor;
|
|
70
|
+
/** Attach this context as soon as the widget loads, and whenever it changes. */
|
|
71
|
+
context?: FeedobackContext;
|
|
72
|
+
/**
|
|
73
|
+
* Remove the widget when the component unmounts. Off by default: the widget
|
|
74
|
+
* is normally mounted once for the app's lifetime.
|
|
75
|
+
*/
|
|
76
|
+
destroyOnUnmount?: boolean;
|
|
77
|
+
/** Called once `widget.js` has loaded. */
|
|
78
|
+
onLoad?: () => void;
|
|
79
|
+
/** Called if `widget.js` fails to load (blocked, offline, wrong host). */
|
|
80
|
+
onError?: () => void;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Loads the Feedoback widget on the page. Render it once, near the root of the
|
|
85
|
+
* app; it draws nothing itself and returns `null`. In Next.js App Router it is
|
|
86
|
+
* a client component, so it can live in a server layout directly.
|
|
87
|
+
*
|
|
88
|
+
* ```tsx
|
|
89
|
+
* <FeedbackWidget projectKey="pk_live_123" />
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
declare function FeedbackWidget({ projectKey, host, version, scriptSrc, visitor, context, destroyOnUnmount, onLoad, onError, }: FeedbackWidgetProps): null;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Convenience wrapper for teams who prefer wrapping the app over dropping a
|
|
96
|
+
* component in a layout. It renders its children and loads the widget beside
|
|
97
|
+
* them; there is no context to consume, because {@link useFeedback} talks to
|
|
98
|
+
* the widget's global directly and works anywhere below (or beside) this.
|
|
99
|
+
*
|
|
100
|
+
* ```tsx
|
|
101
|
+
* <FeedbackProvider projectKey="pk_live_123" visitor={user}>
|
|
102
|
+
* <App />
|
|
103
|
+
* </FeedbackProvider>
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
declare function FeedbackProvider({ children, ...props }: PropsWithChildren<FeedbackWidgetProps>): react.JSX.Element;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns the imperative widget API: `identify`, `open`, `startFeedback`,
|
|
110
|
+
* `destroy` and the rest. The object is stable across renders and works from
|
|
111
|
+
* anywhere the widget is loaded — no provider required, because it proxies to
|
|
112
|
+
* the widget's global. Calls made before the widget finishes loading are
|
|
113
|
+
* queued and replayed.
|
|
114
|
+
*
|
|
115
|
+
* ```tsx
|
|
116
|
+
* const feedback = useFeedback();
|
|
117
|
+
* <button onClick={feedback.startFeedback}>Report a bug</button>
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
declare function useFeedback(): FeedobackApi;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The framework-agnostic core.
|
|
124
|
+
*
|
|
125
|
+
* Everything that touches `window` or the DOM lives here, behind plain
|
|
126
|
+
* functions that take the target `Window` explicitly. That is what makes the
|
|
127
|
+
* behaviour testable without a browser and keeps the React layer a thin shell
|
|
128
|
+
* over it. Nothing here imports React.
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
declare const DEFAULT_HOST = "https://feedoback.com";
|
|
132
|
+
/** The global the hosted widget installs. Keep this name in step with widget.js. */
|
|
133
|
+
declare const GLOBAL: "Feedoback";
|
|
134
|
+
/** A call queued before the widget booted: `[method, arg?]`, replayed on boot. */
|
|
135
|
+
type QueuedCall = [keyof FeedobackApi, unknown?];
|
|
136
|
+
/**
|
|
137
|
+
* The shape `window.Feedoback` can take: a bare queue stub before the script
|
|
138
|
+
* loads, or the full API afterwards. Every method is optional because at any
|
|
139
|
+
* instant only one of those two states is present.
|
|
140
|
+
*/
|
|
141
|
+
type WidgetGlobal = Partial<FeedobackApi> & {
|
|
142
|
+
q?: QueuedCall[];
|
|
143
|
+
};
|
|
144
|
+
type WidgetWindow = Window & {
|
|
145
|
+
[GLOBAL]?: WidgetGlobal;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Builds the imperative {@link FeedobackApi}. Each method is a thin proxy over
|
|
150
|
+
* {@link dispatch}, so the same object works before and after the widget loads
|
|
151
|
+
* and is safe to hold onto across renders.
|
|
152
|
+
*
|
|
153
|
+
* The window is resolved lazily through `getWindow` rather than captured, which
|
|
154
|
+
* keeps the API defined on the server (where there is no window) and lets it
|
|
155
|
+
* start working the moment one exists.
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
declare function createFeedbackApi(getWindow?: () => WidgetWindow | undefined): FeedobackApi;
|
|
159
|
+
|
|
160
|
+
export { DEFAULT_HOST, FeedbackProvider, FeedbackWidget, type FeedbackWidgetProps, type FeedobackApi, type FeedobackContext, type FeedobackVisitor, createFeedbackApi, useFeedback };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRef, useEffect, useMemo } from 'react';
|
|
3
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/FeedbackWidget.tsx
|
|
6
|
+
|
|
7
|
+
// src/core.ts
|
|
8
|
+
var DEFAULT_HOST = "https://feedoback.com";
|
|
9
|
+
var GLOBAL = "Feedoback";
|
|
10
|
+
var SDK_ATTRIBUTE = "data-feedoback-sdk";
|
|
11
|
+
function ensureGlobal(win) {
|
|
12
|
+
return win[GLOBAL] ?? (win[GLOBAL] = { q: [] });
|
|
13
|
+
}
|
|
14
|
+
function dispatch(win, method, arg) {
|
|
15
|
+
const widget = ensureGlobal(win);
|
|
16
|
+
const handler = widget[method];
|
|
17
|
+
if (typeof handler === "function") {
|
|
18
|
+
handler(arg);
|
|
19
|
+
} else {
|
|
20
|
+
(widget.q ?? (widget.q = [])).push([method, arg]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function scriptUrl({ host, scriptSrc }) {
|
|
24
|
+
if (scriptSrc) return scriptSrc;
|
|
25
|
+
const base = (host ?? DEFAULT_HOST).replace(/\/+$/, "");
|
|
26
|
+
return `${base}/widget.js`;
|
|
27
|
+
}
|
|
28
|
+
function existingScript(doc, projectKey) {
|
|
29
|
+
const scripts = doc.querySelectorAll(`script[${SDK_ATTRIBUTE}]`);
|
|
30
|
+
for (const script of scripts) {
|
|
31
|
+
if (script.getAttribute("data-project") === projectKey) return script;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function loadWidget(win, options) {
|
|
36
|
+
const doc = win.document;
|
|
37
|
+
if (!doc) return null;
|
|
38
|
+
ensureGlobal(win);
|
|
39
|
+
const already = existingScript(doc, options.projectKey);
|
|
40
|
+
if (already) return already;
|
|
41
|
+
const script = doc.createElement("script");
|
|
42
|
+
script.src = scriptUrl(options);
|
|
43
|
+
script.async = true;
|
|
44
|
+
script.setAttribute("data-project", options.projectKey);
|
|
45
|
+
if (options.version) script.setAttribute("data-version", options.version);
|
|
46
|
+
script.setAttribute(SDK_ATTRIBUTE, "");
|
|
47
|
+
if (options.onLoad) script.addEventListener("load", options.onLoad, { once: true });
|
|
48
|
+
if (options.onError) script.addEventListener("error", options.onError, { once: true });
|
|
49
|
+
(doc.body ?? doc.head ?? doc.documentElement).appendChild(script);
|
|
50
|
+
return script;
|
|
51
|
+
}
|
|
52
|
+
function unloadWidget(win, script) {
|
|
53
|
+
win[GLOBAL]?.destroy?.();
|
|
54
|
+
script?.remove();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/FeedbackWidget.tsx
|
|
58
|
+
function FeedbackWidget({
|
|
59
|
+
projectKey,
|
|
60
|
+
host,
|
|
61
|
+
version,
|
|
62
|
+
scriptSrc,
|
|
63
|
+
visitor,
|
|
64
|
+
context,
|
|
65
|
+
destroyOnUnmount = false,
|
|
66
|
+
onLoad,
|
|
67
|
+
onError
|
|
68
|
+
}) {
|
|
69
|
+
const scriptRef = useRef(null);
|
|
70
|
+
const onLoadRef = useRef(onLoad);
|
|
71
|
+
const onErrorRef = useRef(onError);
|
|
72
|
+
onLoadRef.current = onLoad;
|
|
73
|
+
onErrorRef.current = onError;
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (typeof window === "undefined") return;
|
|
76
|
+
scriptRef.current = loadWidget(window, {
|
|
77
|
+
projectKey,
|
|
78
|
+
host,
|
|
79
|
+
version,
|
|
80
|
+
scriptSrc,
|
|
81
|
+
onLoad: () => onLoadRef.current?.(),
|
|
82
|
+
onError: () => onErrorRef.current?.()
|
|
83
|
+
});
|
|
84
|
+
return () => {
|
|
85
|
+
if (destroyOnUnmount) {
|
|
86
|
+
unloadWidget(window, scriptRef.current);
|
|
87
|
+
scriptRef.current = null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}, [projectKey, host, version, scriptSrc, destroyOnUnmount]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (typeof window === "undefined" || !visitor) return;
|
|
93
|
+
dispatch(window, "identify", visitor);
|
|
94
|
+
}, [visitor?.id, visitor?.email, visitor?.name]);
|
|
95
|
+
const contextKey = context ? JSON.stringify(context) : null;
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (typeof window === "undefined" || !context) return;
|
|
98
|
+
dispatch(window, "setContext", context);
|
|
99
|
+
}, [contextKey]);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function FeedbackProvider({ children, ...props }) {
|
|
103
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
104
|
+
children,
|
|
105
|
+
/* @__PURE__ */ jsx(FeedbackWidget, { ...props })
|
|
106
|
+
] });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/api.ts
|
|
110
|
+
function resolveWindow() {
|
|
111
|
+
return typeof window !== "undefined" ? window : void 0;
|
|
112
|
+
}
|
|
113
|
+
function createFeedbackApi(getWindow = resolveWindow) {
|
|
114
|
+
const send = (method, arg) => {
|
|
115
|
+
const win = getWindow();
|
|
116
|
+
if (win) dispatch(win, method, arg);
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
identify: (visitor) => send("identify", visitor),
|
|
120
|
+
setContext: (context) => send("setContext", context),
|
|
121
|
+
open: () => send("open"),
|
|
122
|
+
close: () => send("close"),
|
|
123
|
+
toggle: () => send("toggle"),
|
|
124
|
+
startFeedback: () => send("startFeedback"),
|
|
125
|
+
startRecording: () => send("startRecording"),
|
|
126
|
+
openSupport: () => send("openSupport"),
|
|
127
|
+
destroy: () => send("destroy")
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/useFeedback.ts
|
|
132
|
+
function useFeedback() {
|
|
133
|
+
return useMemo(() => createFeedbackApi(), []);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { DEFAULT_HOST, FeedbackProvider, FeedbackWidget, createFeedbackApi, useFeedback };
|
|
137
|
+
//# sourceMappingURL=index.js.map
|
|
138
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core.ts","../src/FeedbackWidget.tsx","../src/FeedbackProvider.tsx","../src/api.ts","../src/useFeedback.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The framework-agnostic core.\n *\n * Everything that touches `window` or the DOM lives here, behind plain\n * functions that take the target `Window` explicitly. That is what makes the\n * behaviour testable without a browser and keeps the React layer a thin shell\n * over it. Nothing here imports React.\n */\nimport type { FeedobackApi, FeedobackVisitor, FeedobackContext } from \"./types\";\n\nexport const DEFAULT_HOST = \"https://feedoback.com\";\n\n/** The global the hosted widget installs. Keep this name in step with widget.js. */\nconst GLOBAL = \"Feedoback\" as const;\n\n/** Our marker attribute, so we recognise a script we injected. */\nconst SDK_ATTRIBUTE = \"data-feedoback-sdk\";\n\n/** A call queued before the widget booted: `[method, arg?]`, replayed on boot. */\ntype QueuedCall = [keyof FeedobackApi, unknown?];\n\n/**\n * The shape `window.Feedoback` can take: a bare queue stub before the script\n * loads, or the full API afterwards. Every method is optional because at any\n * instant only one of those two states is present.\n */\ntype WidgetGlobal = Partial<FeedobackApi> & { q?: QueuedCall[] };\n\nexport type WidgetWindow = Window & { [GLOBAL]?: WidgetGlobal };\n\n/**\n * Ensures the queue stub exists without clobbering a real API the widget may\n * have already installed. `??=` leaves an existing value untouched, so calling\n * this repeatedly is safe.\n */\nfunction ensureGlobal(win: WidgetWindow): WidgetGlobal {\n return (win[GLOBAL] ??= { q: [] });\n}\n\n/**\n * Forwards a call to the widget, or queues it if the widget has not booted.\n *\n * This is the same queue-or-call pattern the hosted widget replays on boot, so\n * a call made a millisecond before `widget.js` finishes loading behaves\n * identically to one made a second after.\n */\nexport function dispatch(win: WidgetWindow, method: keyof FeedobackApi, arg?: unknown): void {\n const widget = ensureGlobal(win);\n const handler = widget[method];\n if (typeof handler === \"function\") {\n (handler as (value?: unknown) => void)(arg);\n } else {\n (widget.q ??= []).push([method, arg]);\n }\n}\n\nexport type LoadOptions = {\n projectKey: string;\n host?: string;\n version?: string;\n scriptSrc?: string;\n onLoad?: () => void;\n onError?: () => void;\n};\n\nfunction scriptUrl({ host, scriptSrc }: LoadOptions): string {\n if (scriptSrc) return scriptSrc;\n const base = (host ?? DEFAULT_HOST).replace(/\\/+$/, \"\");\n return `${base}/widget.js`;\n}\n\n/** Finds a script this SDK already injected for the same project, if any. */\nfunction existingScript(doc: Document, projectKey: string): HTMLScriptElement | null {\n const scripts = doc.querySelectorAll<HTMLScriptElement>(`script[${SDK_ATTRIBUTE}]`);\n for (const script of scripts) {\n if (script.getAttribute(\"data-project\") === projectKey) return script;\n }\n return null;\n}\n\n/**\n * Injects `widget.js` with the attributes the widget reads from its own tag\n * (`data-project`, optional `data-version`). Idempotent: a second call for the\n * same project returns the existing tag rather than stacking a duplicate, which\n * matters under React Strict Mode and single-page navigation.\n *\n * The queue stub is created first, so any identity queued in the same tick is\n * in place before the async script evaluates and replays it.\n */\nexport function loadWidget(win: WidgetWindow, options: LoadOptions): HTMLScriptElement | null {\n const doc = win.document;\n if (!doc) return null;\n\n ensureGlobal(win);\n\n const already = existingScript(doc, options.projectKey);\n if (already) return already;\n\n const script = doc.createElement(\"script\");\n script.src = scriptUrl(options);\n script.async = true;\n script.setAttribute(\"data-project\", options.projectKey);\n if (options.version) script.setAttribute(\"data-version\", options.version);\n script.setAttribute(SDK_ATTRIBUTE, \"\");\n if (options.onLoad) script.addEventListener(\"load\", options.onLoad, { once: true });\n if (options.onError) script.addEventListener(\"error\", options.onError, { once: true });\n\n (doc.body ?? doc.head ?? doc.documentElement).appendChild(script);\n return script;\n}\n\n/**\n * Removes the widget and the script tag this SDK injected. Uses the widget's\n * own `destroy()` when present so it can tear down its shadow hosts and the\n * global; then drops our tag so a later mount starts clean.\n */\nexport function unloadWidget(win: WidgetWindow, script: HTMLScriptElement | null): void {\n win[GLOBAL]?.destroy?.();\n script?.remove();\n}\n","\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { dispatch, loadWidget, unloadWidget } from \"./core\";\nimport type { FeedbackWidgetProps } from \"./types\";\n\n/**\n * Loads the Feedoback widget on the page. Render it once, near the root of the\n * app; it draws nothing itself and returns `null`. In Next.js App Router it is\n * a client component, so it can live in a server layout directly.\n *\n * ```tsx\n * <FeedbackWidget projectKey=\"pk_live_123\" />\n * ```\n */\nexport function FeedbackWidget({\n projectKey,\n host,\n version,\n scriptSrc,\n visitor,\n context,\n destroyOnUnmount = false,\n onLoad,\n onError,\n}: FeedbackWidgetProps): null {\n const scriptRef = useRef<HTMLScriptElement | null>(null);\n\n // Inject the script. Re-runs only when what the tag encodes changes; the\n // load is idempotent, so Strict Mode's double mount does not stack a second\n // launcher. onLoad/onError are read through refs so a new closure each render\n // does not retrigger the injection.\n const onLoadRef = useRef(onLoad);\n const onErrorRef = useRef(onError);\n onLoadRef.current = onLoad;\n onErrorRef.current = onError;\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n scriptRef.current = loadWidget(window, {\n projectKey,\n host,\n version,\n scriptSrc,\n onLoad: () => onLoadRef.current?.(),\n onError: () => onErrorRef.current?.(),\n });\n return () => {\n if (destroyOnUnmount) {\n unloadWidget(window, scriptRef.current);\n scriptRef.current = null;\n }\n };\n }, [projectKey, host, version, scriptSrc, destroyOnUnmount]);\n\n // Identity is kept in its own effect so it re-applies when the signed-in user\n // changes without reloading the script. Queued before boot, forwarded after.\n useEffect(() => {\n if (typeof window === \"undefined\" || !visitor) return;\n dispatch(window, \"identify\", visitor);\n }, [visitor?.id, visitor?.email, visitor?.name]);\n\n const contextKey = context ? JSON.stringify(context) : null;\n useEffect(() => {\n if (typeof window === \"undefined\" || !context) return;\n dispatch(window, \"setContext\", context);\n // context is compared by value through contextKey; re-reading the object\n // here is intentional and matches that comparison.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [contextKey]);\n\n return null;\n}\n","\"use client\";\n\nimport type { PropsWithChildren } from \"react\";\n\nimport { FeedbackWidget } from \"./FeedbackWidget\";\nimport type { FeedbackWidgetProps } from \"./types\";\n\n/**\n * Convenience wrapper for teams who prefer wrapping the app over dropping a\n * component in a layout. It renders its children and loads the widget beside\n * them; there is no context to consume, because {@link useFeedback} talks to\n * the widget's global directly and works anywhere below (or beside) this.\n *\n * ```tsx\n * <FeedbackProvider projectKey=\"pk_live_123\" visitor={user}>\n * <App />\n * </FeedbackProvider>\n * ```\n */\nexport function FeedbackProvider({ children, ...props }: PropsWithChildren<FeedbackWidgetProps>) {\n return (\n <>\n {children}\n <FeedbackWidget {...props} />\n </>\n );\n}\n","/**\n * Builds the imperative {@link FeedobackApi}. Each method is a thin proxy over\n * {@link dispatch}, so the same object works before and after the widget loads\n * and is safe to hold onto across renders.\n *\n * The window is resolved lazily through `getWindow` rather than captured, which\n * keeps the API defined on the server (where there is no window) and lets it\n * start working the moment one exists.\n */\nimport { dispatch, type WidgetWindow } from \"./core\";\nimport type { FeedobackApi } from \"./types\";\n\nfunction resolveWindow(): WidgetWindow | undefined {\n return typeof window !== \"undefined\" ? (window as WidgetWindow) : undefined;\n}\n\nexport function createFeedbackApi(getWindow: () => WidgetWindow | undefined = resolveWindow): FeedobackApi {\n const send = (method: keyof FeedobackApi, arg?: unknown) => {\n const win = getWindow();\n if (win) dispatch(win, method, arg);\n };\n\n return {\n identify: (visitor) => send(\"identify\", visitor),\n setContext: (context) => send(\"setContext\", context),\n open: () => send(\"open\"),\n close: () => send(\"close\"),\n toggle: () => send(\"toggle\"),\n startFeedback: () => send(\"startFeedback\"),\n startRecording: () => send(\"startRecording\"),\n openSupport: () => send(\"openSupport\"),\n destroy: () => send(\"destroy\"),\n };\n}\n","\"use client\";\n\nimport { useMemo } from \"react\";\n\nimport { createFeedbackApi } from \"./api\";\nimport type { FeedobackApi } from \"./types\";\n\n/**\n * Returns the imperative widget API: `identify`, `open`, `startFeedback`,\n * `destroy` and the rest. The object is stable across renders and works from\n * anywhere the widget is loaded — no provider required, because it proxies to\n * the widget's global. Calls made before the widget finishes loading are\n * queued and replayed.\n *\n * ```tsx\n * const feedback = useFeedback();\n * <button onClick={feedback.startFeedback}>Report a bug</button>\n * ```\n */\nexport function useFeedback(): FeedobackApi {\n return useMemo(() => createFeedbackApi(), []);\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "feedoback-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React and Next.js SDK for the Feedoback website feedback and support widget. One component, no manual setup.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Masoud <dev3mike@gmail.com>",
|
|
7
|
+
"homepage": "https://feedoback.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/dev3mike/sidenote.git",
|
|
11
|
+
"directory": "packages/react"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/dev3mike/sidenote/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"feedoback",
|
|
16
|
+
"feedback",
|
|
17
|
+
"widget",
|
|
18
|
+
"react",
|
|
19
|
+
"nextjs",
|
|
20
|
+
"support",
|
|
21
|
+
"bug-report",
|
|
22
|
+
"screen-recording"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"main": "./dist/index.cjs",
|
|
27
|
+
"module": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"require": "./dist/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"dev": "tsup --watch",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"test:e2e": "npm run build && playwright test",
|
|
48
|
+
"prepublishOnly": "npm run typecheck && npm run test && npm run build"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"react": ">=18.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@playwright/test": "^1.62.1",
|
|
55
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
56
|
+
"@testing-library/react": "^16.3.3",
|
|
57
|
+
"@types/react": "^19",
|
|
58
|
+
"@vitejs/plugin-react-swc": "^4.3.3",
|
|
59
|
+
"esbuild": "^0.28.2",
|
|
60
|
+
"jsdom": "^30.0.1",
|
|
61
|
+
"react": "^19.2.8",
|
|
62
|
+
"react-dom": "^19.2.8",
|
|
63
|
+
"tsup": "^8.5.0",
|
|
64
|
+
"typescript": "^5",
|
|
65
|
+
"vitest": "^4.1.11"
|
|
66
|
+
},
|
|
67
|
+
"allowScripts": {
|
|
68
|
+
"@swc/core@1.16.2": true,
|
|
69
|
+
"esbuild@0.28.2": true,
|
|
70
|
+
"esbuild@0.27.7": true
|
|
71
|
+
}
|
|
72
|
+
}
|