@mhosaic/feedback-cli 0.13.0 → 0.14.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.
@@ -0,0 +1,87 @@
1
+ # Consumer install — SvelteKit
2
+
3
+ The CLI's auto-wrap is React-only; for SvelteKit you paste the snippet into the root layout.
4
+
5
+ ---
6
+
7
+ ## Step 1 — Env vars
8
+
9
+ SvelteKit uses Vite's env system but with the `PUBLIC_` prefix for client-readable values. Open `.env.local` and rename:
10
+
11
+ ```diff
12
+ - VITE_FEEDBACK_API_KEY=pk_proj_…
13
+ - VITE_FEEDBACK_ENDPOINT=https://…
14
+ + PUBLIC_FEEDBACK_API_KEY=pk_proj_…
15
+ + PUBLIC_FEEDBACK_ENDPOINT=https://…
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Step 2 — Mount in `+layout.svelte`
21
+
22
+ Edit `src/routes/+layout.svelte`:
23
+
24
+ ```svelte
25
+ <script lang="ts">
26
+ import { onMount } from 'svelte'
27
+ import { env } from '$env/dynamic/public'
28
+ import { createFeedback } from '@mhosaic/feedback'
29
+ import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
30
+ import { withReplay } from '@mhosaic/feedback/replay'
31
+ import { withWebVitals } from '@mhosaic/feedback/webvitals'
32
+
33
+ let fb: ReturnType<typeof createFeedback> | undefined
34
+
35
+ onMount(() => {
36
+ const apiKey = env.PUBLIC_FEEDBACK_API_KEY
37
+ const endpoint = env.PUBLIC_FEEDBACK_ENDPOINT
38
+ if (!apiKey || !endpoint) return
39
+ fb = withErrorTracking(
40
+ withReplay(
41
+ withWebVitals(createFeedback({ apiKey, endpoint, env: import.meta.env.PROD ? 'prod' : 'dev' }))
42
+ )
43
+ )
44
+ })
45
+ </script>
46
+
47
+ <slot />
48
+ ```
49
+
50
+ `onMount` is client-only by definition in Svelte — no SSR import needed.
51
+
52
+ ---
53
+
54
+ ## Step 3 — Wire identify()
55
+
56
+ If your app uses SvelteKit's `+layout.server.ts` to return the user, surface that to the client via the layout `data` and watch it with a `$:` block:
57
+
58
+ ```svelte
59
+ <script lang="ts">
60
+ import { page } from '$app/stores'
61
+ // ... existing imports
62
+
63
+ $: if (fb) {
64
+ const user = $page.data.user
65
+ if (user) fb.identify({ id: user.id, email: user.email, name: user.name })
66
+ else fb.identify({ id: '', email: '', name: '' })
67
+ }
68
+ </script>
69
+ ```
70
+
71
+ For Supabase/Firebase/custom JWT setups, use the snippet from `references/identify-snippets.md` and call it inside the same `onMount` that creates `fb`.
72
+
73
+ ---
74
+
75
+ ## Gotchas
76
+
77
+ - **SSR + window:** the widget touches `window`. Always init inside `onMount` (or after `if (browser) {…}` from `$app/environment`). Never at module top-level.
78
+ - **`$env/dynamic/public` vs `$env/static/public`:** dynamic reads at runtime (works with multiple deploy targets); static inlines at build (better for tree-shaking). Either works for the widget — pick the one your app already uses.
79
+ - **adapter-static / SSG:** the widget hydrates at runtime, so SSG-rendered pages render without the FAB until JS hydrates. Same caveat as Next.js.
80
+
81
+ ---
82
+
83
+ ## Verifying
84
+
85
+ ```bash
86
+ npx @mhosaic/feedback-cli@latest verify --origin http://localhost:5173 --with-test-report
87
+ ```
@@ -0,0 +1,100 @@
1
+ # Consumer install — Vite + React
2
+
3
+ The CLI auto-wires this configuration. Your job is to **verify** the auto-wrap landed, not write the snippet yourself.
4
+
5
+ ---
6
+
7
+ ## After `mhosaic-feedback init` ran
8
+
9
+ The CLI should have:
10
+ 1. Installed `@mhosaic/feedback`
11
+ 2. Written `.env.local` with `VITE_FEEDBACK_API_KEY=…` and `VITE_FEEDBACK_ENDPOINT=…`
12
+ 3. Wrapped `src/main.tsx` with `<FeedbackProvider>` between the marker comments
13
+
14
+ Read `src/main.tsx` and confirm you see:
15
+
16
+ ```typescript
17
+ // === mhosaic-feedback:import:start ===
18
+ import { FeedbackProvider } from '@mhosaic/feedback/react'
19
+ // === mhosaic-feedback:import:end ===
20
+
21
+ // ... existing imports
22
+
23
+ ReactDOM.createRoot(document.getElementById('root')!).render(
24
+ {/* === mhosaic-feedback:wrap:start === */}
25
+ <FeedbackProvider
26
+ apiKey={import.meta.env.VITE_FEEDBACK_API_KEY}
27
+ endpoint={import.meta.env.VITE_FEEDBACK_ENDPOINT}
28
+ env="prod"
29
+ >
30
+ {/* === mhosaic-feedback:wrap:end === */}
31
+ <App />
32
+ {/* === mhosaic-feedback:wrap:start === */}
33
+ </FeedbackProvider>
34
+ {/* === mhosaic-feedback:wrap:end === */}
35
+ )
36
+ ```
37
+
38
+ If any of those markers is missing, the auto-wrap failed (rare — usually because the entry file's structure didn't match the regex). Run `npx @mhosaic/feedback-cli@latest doctor` to confirm. If doctor reports a wiring miss, paste the snippet above into `src/main.tsx` manually, or open an issue on the feedback-tool-mhosaic repo.
39
+
40
+ ---
41
+
42
+ ## Optional: auto-capture middleware
43
+
44
+ The default wrap uses `<FeedbackProvider>`, which doesn't expose the wrapper-chain pattern. To add `withErrorTracking` / `withReplay` / `withWebVitals` on a Vite+React project, replace `<FeedbackProvider>` with a manual `createFeedback()` call in a small client-only module:
45
+
46
+ ```typescript
47
+ // src/feedback.ts
48
+ import { createFeedback } from '@mhosaic/feedback'
49
+ import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
50
+ import { withReplay } from '@mhosaic/feedback/replay'
51
+ import { withWebVitals } from '@mhosaic/feedback/webvitals'
52
+
53
+ export const fb = withErrorTracking(
54
+ withReplay(
55
+ withWebVitals(createFeedback({
56
+ apiKey: import.meta.env.VITE_FEEDBACK_API_KEY,
57
+ endpoint: import.meta.env.VITE_FEEDBACK_ENDPOINT,
58
+ env: import.meta.env.PROD ? 'prod' : 'dev',
59
+ }))
60
+ )
61
+ )
62
+ ```
63
+
64
+ Then in `src/main.tsx` replace the `<FeedbackProvider>` wrap with an import of `./feedback` at the top — so the side effect (widget mount + auto-capture) runs once at module load:
65
+
66
+ ```typescript
67
+ import './feedback' // mounts the widget + arms error/replay/webvitals
68
+ ```
69
+
70
+ And remove the `<FeedbackProvider>` JSX wrap. The widget mounts itself to its own Shadow DOM container; there's no React provider needed unless you want to call `useFeedback()` from components.
71
+
72
+ If you do want `useFeedback()`, keep `<FeedbackProvider>` *and* the manual `createFeedback()` — they cooperate. See `packages/core/src/react/FeedbackProvider.tsx` for the contract.
73
+
74
+ ---
75
+
76
+ ## Env vars
77
+
78
+ ```
79
+ VITE_FEEDBACK_API_KEY=pk_proj_…
80
+ VITE_FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
81
+ ```
82
+
83
+ Vite inlines `import.meta.env.VITE_*` at build time. Don't try to read these at runtime from `process.env` — Vite doesn't expose `process` in browser code.
84
+
85
+ ---
86
+
87
+ ## Gotchas
88
+
89
+ - **`.env.local` is build-time only.** Changes require restarting `pnpm dev`.
90
+ - **Shadow DOM clipping:** the widget mounts to `document.body`. If your app uses `overflow: hidden` on `html` or `body`, the FAB and screen capture may behave oddly. Add a `position: fixed; z-index: 9999;` shim to a root container.
91
+ - **Strict Mode double-effect:** React 18's `<StrictMode>` runs effects twice in dev. The widget is idempotent — second mount is a no-op — but you may see two "widget loaded" console messages in dev. Harmless.
92
+
93
+ ---
94
+
95
+ ## Verifying
96
+
97
+ ```bash
98
+ npx @mhosaic/feedback-cli@latest doctor
99
+ npx @mhosaic/feedback-cli@latest verify --origin http://localhost:5173 --with-test-report
100
+ ```
@@ -0,0 +1,92 @@
1
+ # Consumer install — Vue 3 + Vite
2
+
3
+ The CLI's auto-wrap is React-only; for Vue you paste the snippet into `src/main.ts`.
4
+
5
+ ---
6
+
7
+ ## Step 1 — Env vars
8
+
9
+ The CLI already wrote Vite-flavored env vars. Vue + Vite uses the same `VITE_*` prefix, so **no rename needed**:
10
+
11
+ ```
12
+ VITE_FEEDBACK_API_KEY=pk_proj_…
13
+ VITE_FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Step 2 — Initialize at app boot
19
+
20
+ Edit `src/main.ts`:
21
+
22
+ ```typescript
23
+ import { createApp } from 'vue'
24
+ import App from './App.vue'
25
+
26
+ import { createFeedback } from '@mhosaic/feedback'
27
+ import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
28
+ import { withReplay } from '@mhosaic/feedback/replay'
29
+ import { withWebVitals } from '@mhosaic/feedback/webvitals'
30
+
31
+ const fb = withErrorTracking(
32
+ withReplay(
33
+ withWebVitals(createFeedback({
34
+ apiKey: import.meta.env.VITE_FEEDBACK_API_KEY,
35
+ endpoint: import.meta.env.VITE_FEEDBACK_ENDPOINT,
36
+ env: import.meta.env.PROD ? 'prod' : 'dev',
37
+ }))
38
+ )
39
+ )
40
+
41
+ const app = createApp(App)
42
+ // Optional: expose `this.$feedback` in components and `inject('feedback')` in composables
43
+ app.provide('feedback', fb)
44
+ app.mount('#app')
45
+
46
+ // Export for use in other modules (e.g. your auth store)
47
+ export { fb }
48
+ ```
49
+
50
+ The widget mounts itself to its own Shadow DOM container outside Vue's root. Calling `createFeedback()` once at module load is sufficient — there's no Vue plugin to install.
51
+
52
+ ---
53
+
54
+ ## Step 3 — Wire identify()
55
+
56
+ Use the Supabase / Firebase / custom JWT recipe from `references/identify-snippets.md`. Vue 3's `watchEffect` is the idiomatic place to call `fb.identify()` from a Pinia/Vuex store or a composable:
57
+
58
+ ```typescript
59
+ // src/composables/useAuthIdentity.ts
60
+ import { watchEffect } from 'vue'
61
+ import { useAuthStore } from '@/stores/auth'
62
+ import { fb } from '@/main'
63
+
64
+ export function useFeedbackIdentity() {
65
+ const auth = useAuthStore()
66
+ watchEffect(() => {
67
+ if (auth.user) {
68
+ fb.identify({ id: auth.user.id, email: auth.user.email, name: auth.user.name })
69
+ } else {
70
+ fb.identify({ id: '', email: '', name: '' })
71
+ }
72
+ })
73
+ }
74
+ ```
75
+
76
+ Call `useFeedbackIdentity()` once from a top-level component (e.g. inside `App.vue`'s `<script setup>`).
77
+
78
+ ---
79
+
80
+ ## Gotchas
81
+
82
+ - **Vue Devtools collisions:** none observed in practice; the widget runs in its own Shadow DOM.
83
+ - **`<KeepAlive>` and route changes:** the widget mounts once at module load and persists across all route changes. You don't need to re-mount it.
84
+ - **Nuxt 3:** if the user is on Nuxt (not bare Vue + Vite), surface that — Nuxt has its own SSR rules and the snippet should go in a `client.ts` plugin (`plugins/feedback.client.ts`) instead of `main.ts`. The `.client.ts` suffix tells Nuxt to bundle it as client-only.
85
+
86
+ ---
87
+
88
+ ## Verifying
89
+
90
+ ```bash
91
+ npx @mhosaic/feedback-cli@latest verify --origin http://localhost:5173 --with-test-report
92
+ ```
@@ -0,0 +1,185 @@
1
+ # Consumer mode — installing the widget
2
+
3
+ You are guiding a teammate through installing the @mhosaic/feedback widget into their host app, given a handoff payload from the operator.
4
+
5
+ ---
6
+
7
+ ## Step 1 — Parse the handoff payload
8
+
9
+ Ask the user to paste the handoff payload as plain Markdown. Extract:
10
+
11
+ - `Endpoint:` the backend URL
12
+ - `Public widget key:` the `pk_proj_…` token
13
+ - `Allowed origin(s):` the comma-separated list (used to verify CORS later)
14
+ - `Project slug:` (optional; used only for human-readable error messages)
15
+
16
+ If the user can't find the payload, walk them through asking the operator for it. The format is documented in `SKILL.md` under "handoff payload format".
17
+
18
+ ---
19
+
20
+ ## Step 2 — Classify the host app
21
+
22
+ `Read` the user's `package.json`. Three branches:
23
+
24
+ 1. **package.json exists with `dependencies`** → continue to Step 3 (framework detection).
25
+ 2. **No package.json, but `index.html` / `templates/` / `app/views/` / similar template directory exists** → this is a non-JS host app (Django, Rails, Hugo, plain HTML/static site, etc.). Skip Steps 3-5 and go directly to **`references/consumer-install-plain.md`** (CDN `<script>` flow). Tell the user: "I see this isn't a JS/TS app — the CDN path is the right one. The npm-package + framework-snippet flow doesn't apply."
26
+ 3. **Neither package.json nor any obvious template root** → ask the user with plain text: "I don't see a `package.json` or a template folder. Are you in your host app's project root? What kind of app is this — JS framework, server-rendered (Django/Rails/Hugo), or something else?" Branch based on the answer.
27
+
28
+ ---
29
+
30
+ ## Step 3 — Detect the framework
31
+
32
+ The CLI does framework detection itself, but you should know the result *before* running it so you can decide which post-install snippet (if any) the user needs to apply.
33
+
34
+ Read `package.json`'s `dependencies` + `devDependencies` and classify:
35
+
36
+ | Found | Framework | Reference doc | CLI auto-wires entry? |
37
+ |---|---|---|---|
38
+ | `vite` + `react` + `@vitejs/plugin-react` | `vite-react` | `consumer-install-vite.md` | ✅ yes |
39
+ | `next` | `nextjs` (App or Pages Router — sniff `app/` vs `pages/`) | `consumer-install-next.md` | ❌ paste snippet |
40
+ | `nuxt` | `nuxt` | `consumer-install-nuxt.md` | ❌ paste snippet |
41
+ | `astro` | `astro` | `consumer-install-astro.md` | ❌ paste snippet |
42
+ | `@remix-run/react` | `remix` | `consumer-install-remix.md` | ❌ paste snippet |
43
+ | `vue` ≥ 3 (no `nuxt`) | `vue` | `consumer-install-vue.md` | ❌ paste snippet |
44
+ | `@sveltejs/kit` | `sveltekit` | `consumer-install-svelte.md` | ❌ paste snippet |
45
+ | none of the above | `plain` / `unknown` | `consumer-install-plain.md` | ❌ CDN snippet |
46
+
47
+ Order matters: check for `nuxt` *before* generic `vue`, since Nuxt apps have both. Same for `astro` vs the underlying renderer (Astro apps can also have React/Vue/Svelte).
48
+
49
+ Tell the user which framework you detected. Confirm with `AskUserQuestion` (especially if Next.js, since App vs Pages Router branches differently).
50
+
51
+ ---
52
+
53
+ ## Step 4 — Run the CLI init (non-interactive)
54
+
55
+ Run from the user's project root:
56
+
57
+ ```bash
58
+ npx @mhosaic/feedback-cli@latest init \
59
+ --api-key <pk_proj_…> \
60
+ --endpoint <endpoint> \
61
+ --yes
62
+ ```
63
+
64
+ This will:
65
+ - `npm install @mhosaic/feedback` (using the project's package manager)
66
+ - Write `.env.local` with `VITE_FEEDBACK_API_KEY=…` + `VITE_FEEDBACK_ENDPOINT=…` (Vite-flavored names; rename per framework if Next/Vue/etc. — see framework references)
67
+ - Ensure `.env.local` is in `.gitignore`
68
+ - If `vite-react`: auto-wrap `src/main.tsx` with `<FeedbackProvider>`
69
+
70
+ Surface the CLI's output verbatim. If install fails, stop and read the error message; common causes:
71
+ - No internet
72
+ - Lockfile conflict (offer to delete `pnpm-lock.yaml` / `yarn.lock` / `package-lock.json` and retry)
73
+ - Wrong directory (no `package.json`)
74
+
75
+ ---
76
+
77
+ ## Step 5 — Framework-specific wiring
78
+
79
+ Open the per-framework reference document and follow it verbatim:
80
+
81
+ - Vite + React: `consumer-install-vite.md` (verify the auto-wrap)
82
+ - Next.js: `consumer-install-next.md`
83
+ - Remix: `consumer-install-remix.md`
84
+ - Vue: `consumer-install-vue.md`
85
+ - SvelteKit: `consumer-install-svelte.md`
86
+ - Plain HTML / CDN: `consumer-install-plain.md`
87
+
88
+ Each reference doc gives:
89
+ 1. The env-var prefix (e.g. `NEXT_PUBLIC_FEEDBACK_API_KEY` for Next.js — the CLI's default `VITE_FEEDBACK_*` needs renaming)
90
+ 2. The entry-point snippet to paste (with import + provider/init call)
91
+ 3. Where to put it (exact file path)
92
+ 4. Gotchas (SSR, hydration, client-only)
93
+
94
+ Use the `Edit` tool to make the changes if the user agrees, OR print the snippet and ask the user to apply it manually if they prefer hand control.
95
+
96
+ ---
97
+
98
+ ## Step 6 — Wire identify()
99
+
100
+ Read `references/identify-snippets.md`. Detect the user's auth provider from their dependencies (`@auth0/auth0-react`, `@clerk/clerk-react`, `@supabase/supabase-js`, `firebase`, `next-auth`, `django` session via fetch, custom JWT, anonymous). Paste the matching snippet (with `Edit` tool) into the right place.
101
+
102
+ If you can't detect the auth provider from `package.json`, ask the user explicitly with `AskUserQuestion`.
103
+
104
+ ---
105
+
106
+ ## Step 7 — (Optional but recommended) Wire the auto-capture modules
107
+
108
+ `AskUserQuestion`:
109
+ - question: `Wire the auto-capture modules?`
110
+ - options:
111
+ - `Yes — all three (error-tracking, web-vitals, replay)`, description: `Captures runtime errors, Core Web Vitals, and session replay. ~30 KiB gzip overhead.`
112
+ - `Yes — error-tracking only`, description: `Smallest add. Auto-files synthetic reports for window.onerror + unhandledrejection.`
113
+ - `No — keep it minimal`, description: `Just the manual-submit FAB.`
114
+
115
+ For each `Yes`, modify the consumer's widget init to chain the wrappers. The pattern:
116
+
117
+ ```typescript
118
+ import { createFeedback } from '@mhosaic/feedback'
119
+ import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
120
+ import { withReplay } from '@mhosaic/feedback/replay'
121
+ import { withWebVitals } from '@mhosaic/feedback/webvitals'
122
+
123
+ const fb = withErrorTracking(
124
+ withReplay(
125
+ withWebVitals(createFeedback({ apiKey, endpoint }))
126
+ )
127
+ )
128
+ ```
129
+
130
+ The CLI's auto-wrap uses `<FeedbackProvider>` (a React-specific helper) — for non-Vite-React frameworks the snippet above is the direct equivalent. Match the framework's import style (CommonJS vs ESM, top-level await vs inside an effect).
131
+
132
+ ---
133
+
134
+ ## Step 8 — Run `verify`
135
+
136
+ ```bash
137
+ npx @mhosaic/feedback-cli@latest verify \
138
+ --origin <consumer-dev-origin> \
139
+ --with-test-report
140
+ ```
141
+
142
+ This is a lightweight check; it confirms:
143
+ - `.env.local` has the right shape
144
+ - The endpoint resolves and responds
145
+ - CORS allows the consumer's origin
146
+ - The public key is accepted (POSTs a `[mhosaic-feedback verify]` synthetic report)
147
+
148
+ If anything is red, the verify command prints a hint. Common hints:
149
+ - `add this origin to the project's allowed_origins (admin SPA: Edit project)` → operator needs to add the dev/prod origin.
150
+ - `key invalid or revoked` → operator needs to mint a new key.
151
+
152
+ Stop on red and surface the hint. Don't proceed to step 9 until verify is green.
153
+
154
+ ---
155
+
156
+ ## Step 9 — Live smoke test in Chrome
157
+
158
+ Follow `references/verify-install.md` for the full end-to-end smoke test:
159
+
160
+ 1. Start the consumer's dev server (`pnpm dev` / `npm run dev`)
161
+ 2. Drive Chrome to the dev URL
162
+ 3. Screenshot — confirm the FAB renders (bottom-right corner)
163
+ 4. Click the FAB, fill the form with `[INTEGRATE-FEEDBACK SMOKE TEST]`, submit
164
+ 5. Drive Chrome to `https://software-factory-3tbbu.ondigitalocean.app/reports` and confirm the report landed
165
+
166
+ Both verify (step 8) and smoke test (step 9) write to the report stream — the operator can clean them up after a successful integration (admin SPA → /reports → filter by description → bulk delete).
167
+
168
+ ---
169
+
170
+ ## Step 10 — Done
171
+
172
+ Summarize for the user:
173
+
174
+ - Widget installed ✓
175
+ - FAB renders ✓
176
+ - Smoke-test report received ✓
177
+ - Optional modules wired: [error-tracking / replay / web-vitals / none]
178
+ - Next: deploy to staging, add the staging URL to `allowed_origins` (operator action — DM them the URL)
179
+
180
+ Encourage them to:
181
+ - Commit the change (sans `.env.local`)
182
+ - Tell the operator the smoke test passed
183
+ - Delete the smoke-test reports from `/reports` in admin once it lands
184
+
185
+ If a follow-up question comes up (e.g. "how do I add another origin?"), point them to the operator's `/integrate-feedback` provisioning mode (operator can re-run it with the same project slug to update origins) OR direct the operator to edit the project in the admin SPA at `/projects/<id>`.
@@ -0,0 +1,218 @@
1
+ # identify() recipes per auth provider
2
+
3
+ The widget's FAB is hidden until `fb.identify()` is called with a non-empty `id`. Each snippet below is the minimum-viable wiring for one auth provider. Pick the one that matches the consumer's `package.json`.
4
+
5
+ **General rule:** call `identify()` from a reactive scope that runs *after* the auth SDK resolves the user. Never at module top level. Each snippet below honors that.
6
+
7
+ ---
8
+
9
+ ## 1. Auth0 (`@auth0/auth0-react`)
10
+
11
+ ```tsx
12
+ // src/components/FeedbackIdentity.tsx
13
+ 'use client' // for Next.js App Router; omit for Vite/Remix
14
+
15
+ import { useEffect } from 'react'
16
+ import { useAuth0 } from '@auth0/auth0-react'
17
+ import { useFeedback } from '@mhosaic/feedback/react'
18
+
19
+ export function FeedbackIdentity() {
20
+ const { user, isAuthenticated, isLoading } = useAuth0()
21
+ const fb = useFeedback()
22
+ useEffect(() => {
23
+ if (isLoading) return
24
+ if (isAuthenticated && user) {
25
+ fb.identify({ id: user.sub ?? '', email: user.email, name: user.name })
26
+ } else {
27
+ fb.identify({ id: '', email: '', name: '' })
28
+ }
29
+ }, [fb, isAuthenticated, isLoading, user])
30
+ return null
31
+ }
32
+ ```
33
+
34
+ Mount `<FeedbackIdentity />` once inside both `<Auth0Provider>` and `<FeedbackProvider>` (or wherever you instantiated the widget). Hides the FAB on logout, re-identifies on token refresh.
35
+
36
+ ---
37
+
38
+ ## 2. Clerk (`@clerk/clerk-react`)
39
+
40
+ ```tsx
41
+ import { useEffect } from 'react'
42
+ import { useUser } from '@clerk/clerk-react'
43
+ import { useFeedback } from '@mhosaic/feedback/react'
44
+
45
+ export function FeedbackIdentity() {
46
+ const { isLoaded, isSignedIn, user } = useUser()
47
+ const fb = useFeedback()
48
+ useEffect(() => {
49
+ if (!isLoaded) return
50
+ if (isSignedIn && user) {
51
+ fb.identify({
52
+ id: user.id,
53
+ email: user.primaryEmailAddress?.emailAddress,
54
+ name: user.fullName ?? user.username ?? undefined,
55
+ })
56
+ } else fb.identify({ id: '', email: '', name: '' })
57
+ }, [fb, isLoaded, isSignedIn, user])
58
+ return null
59
+ }
60
+ ```
61
+
62
+ Mount under both `<ClerkProvider>` and your widget provider.
63
+
64
+ ---
65
+
66
+ ## 3. Supabase Auth
67
+
68
+ ```typescript
69
+ // src/lib/feedback.ts (or wherever you instantiated `fb`)
70
+ import { supabase } from './supabaseClient'
71
+ import { fb } from './feedback' // your createFeedback() result
72
+
73
+ supabase.auth.onAuthStateChange((_event, session) => {
74
+ const u = session?.user
75
+ if (u) {
76
+ fb.identify({
77
+ id: u.id,
78
+ email: u.email,
79
+ name: (u.user_metadata?.full_name as string) ?? u.email,
80
+ })
81
+ } else {
82
+ fb.identify({ id: '', email: '', name: '' })
83
+ }
84
+ })
85
+ ```
86
+
87
+ Place this at app boot, after `createFeedback()`. The callback fires once on subscribe with the current session — no separate `getSession()` needed.
88
+
89
+ ---
90
+
91
+ ## 4. Firebase Auth
92
+
93
+ ```typescript
94
+ import { getAuth, onAuthStateChanged } from 'firebase/auth'
95
+ import { fb } from './feedback'
96
+
97
+ onAuthStateChanged(getAuth(), (u) => {
98
+ if (u) {
99
+ fb.identify({
100
+ id: u.uid,
101
+ email: u.email ?? undefined,
102
+ name: u.displayName ?? undefined,
103
+ })
104
+ } else {
105
+ fb.identify({ id: '', email: '', name: '' })
106
+ }
107
+ })
108
+ ```
109
+
110
+ Place after `initializeApp()` and `createFeedback()`. The callback fires immediately with the current user (or `null`).
111
+
112
+ ---
113
+
114
+ ## 5. NextAuth.js / Auth.js (client component)
115
+
116
+ ```tsx
117
+ 'use client'
118
+
119
+ import { useEffect } from 'react'
120
+ import { useSession } from 'next-auth/react'
121
+ import { useFeedback } from '@mhosaic/feedback/react'
122
+
123
+ export function FeedbackIdentity() {
124
+ const { data: session, status } = useSession()
125
+ const fb = useFeedback()
126
+ useEffect(() => {
127
+ if (status === 'loading') return
128
+ const u = session?.user
129
+ if (u) {
130
+ fb.identify({
131
+ id: (u as { id?: string }).id ?? u.email ?? '',
132
+ email: u.email ?? undefined,
133
+ name: u.name ?? undefined,
134
+ })
135
+ } else fb.identify({ id: '', email: '', name: '' })
136
+ }, [fb, session, status])
137
+ return null
138
+ }
139
+ ```
140
+
141
+ Mount under `<SessionProvider>`. NextAuth's default `session.user` has no `id` — either add one via the `session` callback in your `[...nextauth]/route.ts`, or fall back to `email` (as above).
142
+
143
+ ---
144
+
145
+ ## 6. Django session-based auth
146
+
147
+ ```typescript
148
+ import { fb } from './feedback'
149
+
150
+ async function syncFeedbackIdentity() {
151
+ try {
152
+ const res = await fetch('/api/auth/me/', { credentials: 'include' })
153
+ if (!res.ok) { fb.identify({ id: '', email: '', name: '' }); return }
154
+ const u = await res.json()
155
+ fb.identify({ id: String(u.id), email: u.email, name: u.name })
156
+ } catch {
157
+ fb.identify({ id: '', email: '', name: '' })
158
+ }
159
+ }
160
+ syncFeedbackIdentity()
161
+ // Re-call from your login-success + logout handlers
162
+ ```
163
+
164
+ No push channel from Django sessions — you call this at boot, on login, on logout. If you have a SWR/React Query hook for `/api/auth/me/`, mirror the identity call into the same hook's `onSuccess`.
165
+
166
+ ---
167
+
168
+ ## 7. Custom JWT in `localStorage`
169
+
170
+ ```typescript
171
+ import { fb } from './feedback'
172
+
173
+ function decodeJwt(t: string): { sub?: string; email?: string; name?: string; exp?: number } {
174
+ return JSON.parse(atob(t.split('.')[1]!))
175
+ }
176
+
177
+ function syncIdentity() {
178
+ const token = localStorage.getItem('jwt')
179
+ if (!token) return fb.identify({ id: '', email: '', name: '' })
180
+ try {
181
+ const { sub, email, name, exp } = decodeJwt(token)
182
+ if (exp && exp * 1000 < Date.now()) return fb.identify({ id: '', email: '', name: '' })
183
+ fb.identify({ id: String(sub ?? ''), email, name })
184
+ } catch { fb.identify({ id: '', email: '', name: '' }) }
185
+ }
186
+ syncIdentity()
187
+ window.addEventListener('storage', (e) => { if (e.key === 'jwt') syncIdentity() })
188
+ ```
189
+
190
+ The `storage` listener catches logout-in-another-tab. Decoding without verification is fine because the widget identity is not a trust boundary — the backend re-verifies on report submission. For attested identity (`userHash` + `exp` HMAC), compute the envelope server-side and pass it through verbatim.
191
+
192
+ ---
193
+
194
+ ## 8. Anonymous-only (no auth)
195
+
196
+ ```typescript
197
+ import { fb } from './feedback'
198
+
199
+ // FAB only renders for non-empty `id`. Use a stable browser id so reports
200
+ // from the same anonymous visitor cluster together on the dashboard.
201
+ const id = localStorage.getItem('mhosaic-anon-id')
202
+ ?? (() => {
203
+ const fresh = `anon-${crypto.randomUUID()}`
204
+ localStorage.setItem('mhosaic-anon-id', fresh)
205
+ return fresh
206
+ })()
207
+ fb.identify({ id, name: 'Anonymous' })
208
+ ```
209
+
210
+ Place immediately after `createFeedback()`. Module-top-level is fine here — no async wait needed.
211
+
212
+ ---
213
+
214
+ ## Notes
215
+
216
+ - **Hide the FAB on logout:** every snippet calls `identify({id: '', …})` on the logged-out branch. The widget's visibility gate (see `packages/core/src/core.ts:154`) hides the FAB whenever `id` is empty.
217
+ - **Re-identify on token refresh is free.** Each call swaps the closure-held user reference; there's no remount overhead.
218
+ - **Signed identity (`userHash` + `exp`) is not covered here** because it requires the host backend to compute an HMAC envelope with the project's signing secret. Issue the secret via `python manage.py feedback_issue_signing_secret --project <slug>`, store it in the host's vault, sign on every page render, and pass `userHash` + `exp` through to `identify()` verbatim alongside `id` / `email` / `name`.