@verbumia/feedback 0.2.5 → 0.2.7

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.
@@ -1,17 +1,43 @@
1
1
  import { ReactNode } from 'react';
2
- import { F as FeedbackClient, D as DeclaredKey } from '../client-CYgwLkiA.cjs';
3
- export { B as BatchResponse, a as FeedbackConfig, b as FeedbackError, c as FeedbackString, R as RatingInput, S as StringsResponse, d as SuggestionInput, T as TokenBundle } from '../client-CYgwLkiA.cjs';
2
+ import { F as FeedbackClient, D as DeclaredKey } from '../client-D83qhH0O.cjs';
3
+ export { B as BatchResponse, a as FeedbackConfig, b as FeedbackError, c as FeedbackString, R as RatingInput, S as StringsResponse, d as SuggestionInput, T as TokenBundle } from '../client-D83qhH0O.cjs';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
- export { h as hasKeyRegistry, r as resolveKeys } from '../keys-BQpczt84.cjs';
5
+ export { h as hasKeyRegistry, r as resolveKeys } from '../keys-D4oJtn64.cjs';
6
6
 
7
7
  /** Structural mirror of `@verbumia/react-i18next`'s VerbumiaPlugin —
8
- * kept local so feedback has no build-time dep on the i18n SDK. */
8
+ * kept local so feedback has no build-time dep on the i18n SDK. The
9
+ * `i18n` and `onLanguageChange` fields are optional so a plugin attached
10
+ * to a non-Verbumia host (or to a pre-1.0.5 react-i18next) keeps
11
+ * working — runtime language re-sync is just disabled. From feedback
12
+ * ≥0.2.6, the peerDep on `@verbumia/react-i18next` is `>=1.0.5`, so the
13
+ * lang-change subscription path is guaranteed available in matched
14
+ * installs. */
9
15
  interface I18nPluginContext {
16
+ i18n?: {
17
+ language?: string;
18
+ /** 0.2.7 — direct handle on the underlying `i18next` instance the
19
+ * `@verbumia/react-i18next` provider exposes. Used by the
20
+ * `scope: "current-view"` snapshot path: a same-locale
21
+ * `changeLanguage(language)` re-emits `languageChanged` so every
22
+ * bound consumer re-renders → `t()` calls repopulate the registry.
23
+ * Optional so the plugin still works on non-Verbumia hosts (it
24
+ * falls back to a no-rerender registry snapshot in that case). */
25
+ i18next?: {
26
+ language?: string;
27
+ changeLanguage?: (lng: string) => Promise<unknown>;
28
+ };
29
+ };
10
30
  config: {
11
31
  apiBase?: string;
12
32
  projectUuid: string;
13
33
  defaultLocale: string;
14
34
  };
35
+ /** #806 — subscribe to runtime language changes (see VerbumiaPluginContext
36
+ * in `@verbumia/react-i18next` ≥1.0.5). Returns an unsubscribe fn the
37
+ * plugin MUST call from teardown. Optional so attaching to a
38
+ * pre-1.0.5 react-i18next falls back gracefully (no auto re-sync;
39
+ * consumer can still pass `options.language` explicitly). */
40
+ onLanguageChange?: (cb: (lng: string) => void) => () => void;
15
41
  }
16
42
  interface I18nPlugin {
17
43
  name: string;
@@ -19,10 +45,53 @@ interface I18nPlugin {
19
45
  render?: () => ReactNode;
20
46
  }
21
47
  interface FeedbackController {
22
- open: () => void;
48
+ /**
49
+ * Opens the panel. From 0.2.7 the call is **async** (returns
50
+ * `Promise<void>`) so the `scope: "current-view"` snapshot sequence
51
+ * can run before the modal mounts (reset registry → force
52
+ * languageChanged → 1-frame yield → snapshot). Hosts that do NOT
53
+ * await still work — the modal mounts when the promise resolves.
54
+ * Under `scope: "all"` (or when the i18next instance isn't reachable)
55
+ * `open()` resolves on the next microtask with no extra work.
56
+ */
57
+ open: () => Promise<void>;
23
58
  close: () => void;
24
59
  /** The underlying client (advanced; usually unused). */
25
60
  client: FeedbackClient;
61
+ /** ToS version currently required by the backend (SDK build-time
62
+ * constant — see core/tos.ts). 0.2.7+. */
63
+ readonly tosVersion: string;
64
+ /** Whether this end-user has a live session-token bundle (i.e. has
65
+ * accepted the current ToS version). Mirrors the same persisted state
66
+ * the built-in modal writes to, so an external ToS page that calls
67
+ * `acceptTos()` flips this to `true` immediately. 0.2.7+. */
68
+ readonly hasAcceptedTos: boolean;
69
+ /** Programmatically POST `/v1/feedback/tos` and persist the returned
70
+ * token bundle into the SDK's shared store. Used by hosts that
71
+ * build their own ToS page (`feedbackPlugin({ tos: "skip" })`). Thin
72
+ * alias over `FeedbackClient.acceptTos()` — idempotent (a second
73
+ * call returns the in-flight / existing bundle without re-POSTing).
74
+ * 0.2.7+. */
75
+ acceptTos: () => Promise<void>;
76
+ /** 0.2.7 — feedback-addon active flag for this project, sourced from
77
+ * `GET /v1/projects/{p}/feedback-addon/state` + composed with the
78
+ * plugin's `cta` option:
79
+ * - `cta: "auto"` (default): mirrors `state.isActive` (so a host
80
+ * that hides its CTA on `!isActive` does the right thing for
81
+ * Starter / no SKU).
82
+ * - `cta: "show"`: always `true` (force the host's CTA).
83
+ * - `cta: "hide"`: always `false`.
84
+ * `null` before the first state fetch resolves (e.g. on a host that
85
+ * didn't pass `apiKey` and hasn't accepted ToS yet). */
86
+ readonly isActive: boolean | null;
87
+ /** 0.2.7 — the languages the SDK is licensed to rate strings in.
88
+ * Mirrors `state.enabledLanguages` verbatim: `string[]` for Starter
89
+ * / no SKU, literal `"all"` for Unlimited. `null` before the first
90
+ * state fetch resolves. */
91
+ readonly enabledLanguages: string[] | "all" | null;
92
+ /** 0.2.7 — raw SKU code (`feedback_starter` / `feedback_unlimited`
93
+ * / `null`). Useful for downstream UI / billing dashboards. */
94
+ readonly sku: "feedback_starter" | "feedback_unlimited" | null;
26
95
  }
27
96
  interface FeedbackPluginOptions {
28
97
  /** Override language; defaults to the i18n provider's defaultLocale. */
@@ -48,6 +117,62 @@ interface FeedbackPluginOptions {
48
117
  };
49
118
  /** Injected fetch (tests / RN polyfills). */
50
119
  fetchImpl?: typeof fetch;
120
+ /**
121
+ * 0.2.7 — how the SDK handles the Terms of Service prompt:
122
+ * - `"modal"` (DEFAULT, backwards-compatible): the built-in modal
123
+ * renders the SDK's ToS step on first open; tapping Accept calls
124
+ * `POST /v1/feedback/tos` and persists the token bundle.
125
+ * - `"skip"`: the built-in modal SKIPS the ToS step entirely. The
126
+ * host promises to handle consent externally (e.g. their own
127
+ * branded ToS page) and must call `controller.acceptTos()` to
128
+ * mint the session bundle. Until that runs, every `getStrings()`
129
+ * surfaces the existing 401 error state (the SDK does NOT
130
+ * silently auto-accept on the user's behalf).
131
+ */
132
+ tos?: "modal" | "skip";
133
+ /**
134
+ * 0.2.7 — host's project API key (`vrb_live_…`, scope `project:read`).
135
+ * Lets the SDK call `GET /v1/projects/{p}/feedback-addon/state` at
136
+ * `setup()` time + on language change, BEFORE the end-user accepts
137
+ * ToS. When omitted, addon-state is deferred until the user's
138
+ * session bearer is minted via `acceptTos()`, and `controller.isActive`
139
+ * stays `null` until then.
140
+ */
141
+ apiKey?: string;
142
+ /**
143
+ * 0.2.7 — controls `controller.isActive` (which hosts wire to their
144
+ * own CTA's `hidden` flag — the SDK does not render a floating CTA
145
+ * itself; see plugin.tsx header).
146
+ * - `"auto"` (DEFAULT): mirrors `state.isActive` from the backend,
147
+ * i.e. hides on Starter / no SKU, shows on Unlimited.
148
+ * - `"show"`: forces `isActive=true` regardless of state.
149
+ * - `"hide"`: forces `isActive=false` regardless of state.
150
+ */
151
+ cta?: "auto" | "show" | "hide";
152
+ /**
153
+ * 0.2.7 — what the panel calls "on-screen keys":
154
+ * - `"current-view"` (DEFAULT — rebalances the 1.0.3 "strict-better-
155
+ * than-false-empty" trade-off now that we have a better mechanism):
156
+ * `controller.open()` (now async) resets the on-screen key
157
+ * registry, force-emits a same-locale `languageChanged` through
158
+ * i18next to re-render every consumer, awaits a 1-frame yield so
159
+ * their `t()` calls repopulate the registry, snapshots, then opens
160
+ * the modal with that snapshot. Result: the widget lists ONLY keys
161
+ * rendered on the user's current screen at the instant they
162
+ * opened it.
163
+ * - `"all"`: pre-0.2.7 behavior — no reset, no force re-render. The
164
+ * panel reads the accumulated registry as-is (still strictly
165
+ * better than a false-empty; useful if you want the cumulative
166
+ * keys-since-mount view).
167
+ *
168
+ * KNOWN LIMITATION (`"current-view"`): components that call
169
+ * `i18next.t()` OUTSIDE a React tree (cron-like calls, module-load
170
+ * `t()`) are NOT re-collected by the changeLanguage trigger — they
171
+ * have nothing to re-render. Acceptable: those calls aren't strictly
172
+ * "rendered on screen", so excluding them matches the on-screen
173
+ * contract.
174
+ */
175
+ scope?: "current-view" | "all";
51
176
  }
52
177
  declare function feedbackPlugin(options: FeedbackPluginOptions): I18nPlugin;
53
178
 
@@ -55,6 +180,11 @@ declare function FeedbackPanel(props: {
55
180
  client: FeedbackClient;
56
181
  keys?: DeclaredKey[];
57
182
  namespace?: string | string[];
183
+ /** 0.2.7 — `"skip"` removes the built-in ToS step entirely; the host
184
+ * owns consent (via `controller.acceptTos()`). When the user has not
185
+ * yet accepted, the strings fetch surfaces a 401-derived error in the
186
+ * existing error row. */
187
+ tos?: "modal" | "skip";
58
188
  onClose: () => void;
59
189
  }): react_jsx_runtime.JSX.Element;
60
190
 
@@ -1,17 +1,43 @@
1
1
  import { ReactNode } from 'react';
2
- import { F as FeedbackClient, D as DeclaredKey } from '../client-CYgwLkiA.js';
3
- export { B as BatchResponse, a as FeedbackConfig, b as FeedbackError, c as FeedbackString, R as RatingInput, S as StringsResponse, d as SuggestionInput, T as TokenBundle } from '../client-CYgwLkiA.js';
2
+ import { F as FeedbackClient, D as DeclaredKey } from '../client-D83qhH0O.js';
3
+ export { B as BatchResponse, a as FeedbackConfig, b as FeedbackError, c as FeedbackString, R as RatingInput, S as StringsResponse, d as SuggestionInput, T as TokenBundle } from '../client-D83qhH0O.js';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
- export { h as hasKeyRegistry, r as resolveKeys } from '../keys-BPAFf8xR.js';
5
+ export { h as hasKeyRegistry, r as resolveKeys } from '../keys-BpVB1kcI.js';
6
6
 
7
7
  /** Structural mirror of `@verbumia/react-i18next`'s VerbumiaPlugin —
8
- * kept local so feedback has no build-time dep on the i18n SDK. */
8
+ * kept local so feedback has no build-time dep on the i18n SDK. The
9
+ * `i18n` and `onLanguageChange` fields are optional so a plugin attached
10
+ * to a non-Verbumia host (or to a pre-1.0.5 react-i18next) keeps
11
+ * working — runtime language re-sync is just disabled. From feedback
12
+ * ≥0.2.6, the peerDep on `@verbumia/react-i18next` is `>=1.0.5`, so the
13
+ * lang-change subscription path is guaranteed available in matched
14
+ * installs. */
9
15
  interface I18nPluginContext {
16
+ i18n?: {
17
+ language?: string;
18
+ /** 0.2.7 — direct handle on the underlying `i18next` instance the
19
+ * `@verbumia/react-i18next` provider exposes. Used by the
20
+ * `scope: "current-view"` snapshot path: a same-locale
21
+ * `changeLanguage(language)` re-emits `languageChanged` so every
22
+ * bound consumer re-renders → `t()` calls repopulate the registry.
23
+ * Optional so the plugin still works on non-Verbumia hosts (it
24
+ * falls back to a no-rerender registry snapshot in that case). */
25
+ i18next?: {
26
+ language?: string;
27
+ changeLanguage?: (lng: string) => Promise<unknown>;
28
+ };
29
+ };
10
30
  config: {
11
31
  apiBase?: string;
12
32
  projectUuid: string;
13
33
  defaultLocale: string;
14
34
  };
35
+ /** #806 — subscribe to runtime language changes (see VerbumiaPluginContext
36
+ * in `@verbumia/react-i18next` ≥1.0.5). Returns an unsubscribe fn the
37
+ * plugin MUST call from teardown. Optional so attaching to a
38
+ * pre-1.0.5 react-i18next falls back gracefully (no auto re-sync;
39
+ * consumer can still pass `options.language` explicitly). */
40
+ onLanguageChange?: (cb: (lng: string) => void) => () => void;
15
41
  }
16
42
  interface I18nPlugin {
17
43
  name: string;
@@ -19,10 +45,53 @@ interface I18nPlugin {
19
45
  render?: () => ReactNode;
20
46
  }
21
47
  interface FeedbackController {
22
- open: () => void;
48
+ /**
49
+ * Opens the panel. From 0.2.7 the call is **async** (returns
50
+ * `Promise<void>`) so the `scope: "current-view"` snapshot sequence
51
+ * can run before the modal mounts (reset registry → force
52
+ * languageChanged → 1-frame yield → snapshot). Hosts that do NOT
53
+ * await still work — the modal mounts when the promise resolves.
54
+ * Under `scope: "all"` (or when the i18next instance isn't reachable)
55
+ * `open()` resolves on the next microtask with no extra work.
56
+ */
57
+ open: () => Promise<void>;
23
58
  close: () => void;
24
59
  /** The underlying client (advanced; usually unused). */
25
60
  client: FeedbackClient;
61
+ /** ToS version currently required by the backend (SDK build-time
62
+ * constant — see core/tos.ts). 0.2.7+. */
63
+ readonly tosVersion: string;
64
+ /** Whether this end-user has a live session-token bundle (i.e. has
65
+ * accepted the current ToS version). Mirrors the same persisted state
66
+ * the built-in modal writes to, so an external ToS page that calls
67
+ * `acceptTos()` flips this to `true` immediately. 0.2.7+. */
68
+ readonly hasAcceptedTos: boolean;
69
+ /** Programmatically POST `/v1/feedback/tos` and persist the returned
70
+ * token bundle into the SDK's shared store. Used by hosts that
71
+ * build their own ToS page (`feedbackPlugin({ tos: "skip" })`). Thin
72
+ * alias over `FeedbackClient.acceptTos()` — idempotent (a second
73
+ * call returns the in-flight / existing bundle without re-POSTing).
74
+ * 0.2.7+. */
75
+ acceptTos: () => Promise<void>;
76
+ /** 0.2.7 — feedback-addon active flag for this project, sourced from
77
+ * `GET /v1/projects/{p}/feedback-addon/state` + composed with the
78
+ * plugin's `cta` option:
79
+ * - `cta: "auto"` (default): mirrors `state.isActive` (so a host
80
+ * that hides its CTA on `!isActive` does the right thing for
81
+ * Starter / no SKU).
82
+ * - `cta: "show"`: always `true` (force the host's CTA).
83
+ * - `cta: "hide"`: always `false`.
84
+ * `null` before the first state fetch resolves (e.g. on a host that
85
+ * didn't pass `apiKey` and hasn't accepted ToS yet). */
86
+ readonly isActive: boolean | null;
87
+ /** 0.2.7 — the languages the SDK is licensed to rate strings in.
88
+ * Mirrors `state.enabledLanguages` verbatim: `string[]` for Starter
89
+ * / no SKU, literal `"all"` for Unlimited. `null` before the first
90
+ * state fetch resolves. */
91
+ readonly enabledLanguages: string[] | "all" | null;
92
+ /** 0.2.7 — raw SKU code (`feedback_starter` / `feedback_unlimited`
93
+ * / `null`). Useful for downstream UI / billing dashboards. */
94
+ readonly sku: "feedback_starter" | "feedback_unlimited" | null;
26
95
  }
27
96
  interface FeedbackPluginOptions {
28
97
  /** Override language; defaults to the i18n provider's defaultLocale. */
@@ -48,6 +117,62 @@ interface FeedbackPluginOptions {
48
117
  };
49
118
  /** Injected fetch (tests / RN polyfills). */
50
119
  fetchImpl?: typeof fetch;
120
+ /**
121
+ * 0.2.7 — how the SDK handles the Terms of Service prompt:
122
+ * - `"modal"` (DEFAULT, backwards-compatible): the built-in modal
123
+ * renders the SDK's ToS step on first open; tapping Accept calls
124
+ * `POST /v1/feedback/tos` and persists the token bundle.
125
+ * - `"skip"`: the built-in modal SKIPS the ToS step entirely. The
126
+ * host promises to handle consent externally (e.g. their own
127
+ * branded ToS page) and must call `controller.acceptTos()` to
128
+ * mint the session bundle. Until that runs, every `getStrings()`
129
+ * surfaces the existing 401 error state (the SDK does NOT
130
+ * silently auto-accept on the user's behalf).
131
+ */
132
+ tos?: "modal" | "skip";
133
+ /**
134
+ * 0.2.7 — host's project API key (`vrb_live_…`, scope `project:read`).
135
+ * Lets the SDK call `GET /v1/projects/{p}/feedback-addon/state` at
136
+ * `setup()` time + on language change, BEFORE the end-user accepts
137
+ * ToS. When omitted, addon-state is deferred until the user's
138
+ * session bearer is minted via `acceptTos()`, and `controller.isActive`
139
+ * stays `null` until then.
140
+ */
141
+ apiKey?: string;
142
+ /**
143
+ * 0.2.7 — controls `controller.isActive` (which hosts wire to their
144
+ * own CTA's `hidden` flag — the SDK does not render a floating CTA
145
+ * itself; see plugin.tsx header).
146
+ * - `"auto"` (DEFAULT): mirrors `state.isActive` from the backend,
147
+ * i.e. hides on Starter / no SKU, shows on Unlimited.
148
+ * - `"show"`: forces `isActive=true` regardless of state.
149
+ * - `"hide"`: forces `isActive=false` regardless of state.
150
+ */
151
+ cta?: "auto" | "show" | "hide";
152
+ /**
153
+ * 0.2.7 — what the panel calls "on-screen keys":
154
+ * - `"current-view"` (DEFAULT — rebalances the 1.0.3 "strict-better-
155
+ * than-false-empty" trade-off now that we have a better mechanism):
156
+ * `controller.open()` (now async) resets the on-screen key
157
+ * registry, force-emits a same-locale `languageChanged` through
158
+ * i18next to re-render every consumer, awaits a 1-frame yield so
159
+ * their `t()` calls repopulate the registry, snapshots, then opens
160
+ * the modal with that snapshot. Result: the widget lists ONLY keys
161
+ * rendered on the user's current screen at the instant they
162
+ * opened it.
163
+ * - `"all"`: pre-0.2.7 behavior — no reset, no force re-render. The
164
+ * panel reads the accumulated registry as-is (still strictly
165
+ * better than a false-empty; useful if you want the cumulative
166
+ * keys-since-mount view).
167
+ *
168
+ * KNOWN LIMITATION (`"current-view"`): components that call
169
+ * `i18next.t()` OUTSIDE a React tree (cron-like calls, module-load
170
+ * `t()`) are NOT re-collected by the changeLanguage trigger — they
171
+ * have nothing to re-render. Acceptable: those calls aren't strictly
172
+ * "rendered on screen", so excluding them matches the on-screen
173
+ * contract.
174
+ */
175
+ scope?: "current-view" | "all";
51
176
  }
52
177
  declare function feedbackPlugin(options: FeedbackPluginOptions): I18nPlugin;
53
178
 
@@ -55,6 +180,11 @@ declare function FeedbackPanel(props: {
55
180
  client: FeedbackClient;
56
181
  keys?: DeclaredKey[];
57
182
  namespace?: string | string[];
183
+ /** 0.2.7 — `"skip"` removes the built-in ToS step entirely; the host
184
+ * owns consent (via `controller.acceptTos()`). When the user has not
185
+ * yet accepted, the strings fetch surfaces a 401-derived error in the
186
+ * existing error row. */
187
+ tos?: "modal" | "skip";
58
188
  onClose: () => void;
59
189
  }): react_jsx_runtime.JSX.Element;
60
190
 
@@ -4,7 +4,7 @@ import {
4
4
  FeedbackError,
5
5
  hasKeyRegistry,
6
6
  resolveKeys
7
- } from "../chunk-5DZUKJ4M.js";
7
+ } from "../chunk-7KWEI55W.js";
8
8
 
9
9
  // src/react/plugin.tsx
10
10
  import { createPortal } from "react-dom";
@@ -23,8 +23,10 @@ var C = {
23
23
  emeraldSoft: "#34d399"
24
24
  };
25
25
  function FeedbackPanel(props) {
26
- const { client, keys, namespace, onClose } = props;
27
- const [consented, setConsented] = useState(client.hasConsented);
26
+ const { client, keys, namespace, tos = "modal", onClose } = props;
27
+ const [consented, setConsented] = useState(
28
+ tos === "skip" ? true : client.hasConsented
29
+ );
28
30
  const [busy, setBusy] = useState(false);
29
31
  const [error, setError] = useState(null);
30
32
  const [strings, setStrings] = useState([]);
@@ -310,42 +312,73 @@ function StringRow(props) {
310
312
  // src/react/plugin.tsx
311
313
  import { jsx as jsx2 } from "react/jsx-runtime";
312
314
  function makeStore() {
313
- let open = false;
315
+ let state = { isOpen: false, snapshotKeys: void 0 };
314
316
  const listeners = /* @__PURE__ */ new Set();
315
317
  return {
316
- isOpen: () => open,
317
- set(v) {
318
- if (open !== v) {
319
- open = v;
320
- listeners.forEach((l) => l());
318
+ getState: () => state,
319
+ setOpen(open, snapshotKeys) {
320
+ const next = {
321
+ isOpen: open,
322
+ snapshotKeys: open ? snapshotKeys : void 0
323
+ };
324
+ if (state.isOpen === next.isOpen && state.snapshotKeys === next.snapshotKeys) {
325
+ return;
321
326
  }
327
+ state = next;
328
+ listeners.forEach((l) => l());
322
329
  },
323
330
  subscribe(l) {
324
331
  listeners.add(l);
325
- return () => listeners.delete(l);
332
+ return () => {
333
+ listeners.delete(l);
334
+ };
326
335
  }
327
336
  };
328
337
  }
338
+ async function captureCurrentViewSnapshot(i18next) {
339
+ const reg = globalThis.__verbumia_key_registry__;
340
+ if (!reg) return [];
341
+ const lng = i18next?.language;
342
+ const change = i18next?.changeLanguage;
343
+ if (typeof change === "function" && typeof lng === "string" && lng) {
344
+ reg.reset?.();
345
+ try {
346
+ await change(lng);
347
+ } catch {
348
+ }
349
+ await new Promise((r) => {
350
+ if (typeof requestAnimationFrame === "function") {
351
+ requestAnimationFrame(() => r());
352
+ } else {
353
+ setTimeout(() => r(), 16);
354
+ }
355
+ });
356
+ return reg.snapshot();
357
+ }
358
+ return reg.snapshot();
359
+ }
329
360
  function feedbackPlugin(options) {
330
361
  const store = makeStore();
331
362
  let client = null;
332
363
  function Outlet() {
333
- const isOpen = useSyncExternalStore(
364
+ const state = useSyncExternalStore(
334
365
  store.subscribe,
335
- store.isOpen,
336
- store.isOpen
366
+ store.getState,
367
+ store.getState
337
368
  );
338
- if (!isOpen || !client || typeof document === "undefined") return null;
369
+ if (!state.isOpen || !client || typeof document === "undefined") return null;
339
370
  const c = client;
371
+ const panelKeys = options.keys ?? state.snapshotKeys;
340
372
  return createPortal(
341
373
  /* @__PURE__ */ jsx2(
342
374
  FeedbackPanel,
343
375
  {
344
376
  client: c,
345
- keys: options.keys,
377
+ keys: panelKeys,
346
378
  namespace: options.namespace,
379
+ tos: options.tos ?? "modal",
347
380
  onClose: () => {
348
- store.set(false);
381
+ store.setOpen(false);
349
382
  void c.flush();
350
383
  }
351
384
  }
@@ -356,24 +389,77 @@ function feedbackPlugin(options) {
356
389
  return {
357
390
  name: "@verbumia/feedback",
358
391
  setup(ctx) {
392
+ const initialLanguage = options.language ?? ctx.i18n?.language ?? ctx.config.defaultLocale;
393
+ const tos = options.tos ?? "modal";
359
394
  client = new FeedbackClient({
360
395
  apiBase: options.apiBase ?? ctx.config.apiBase ?? "https://api.verbumia.dev",
361
396
  projectId: options.projectId ?? ctx.config.projectUuid,
362
- language: options.language ?? ctx.config.defaultLocale,
397
+ language: initialLanguage,
363
398
  endUserId: options.endUserId,
364
- fetchImpl: options.fetchImpl
399
+ fetchImpl: options.fetchImpl,
400
+ apiKey: options.apiKey,
401
+ autoAcceptTos: tos !== "skip"
365
402
  });
403
+ const clientRef = client;
404
+ let addonState = null;
405
+ const cta = options.cta ?? "auto";
406
+ const scope = options.scope ?? "current-view";
407
+ const ctxI18next = ctx.i18n?.i18next;
408
+ const refreshState = async () => {
409
+ try {
410
+ const next = await clientRef.getAddonState();
411
+ if (next !== null) addonState = next;
412
+ } catch {
413
+ }
414
+ };
415
+ if (options.apiKey) void refreshState();
416
+ let langUnsub;
417
+ if (typeof ctx.onLanguageChange === "function") {
418
+ langUnsub = ctx.onLanguageChange((lng) => {
419
+ clientRef.setLanguage(lng);
420
+ void refreshState();
421
+ });
422
+ }
366
423
  const controller = {
367
- open: () => store.set(true),
424
+ open: async () => {
425
+ if (scope === "current-view") {
426
+ const snapshot = await captureCurrentViewSnapshot(ctxI18next);
427
+ store.setOpen(true, snapshot);
428
+ return;
429
+ }
430
+ store.setOpen(true);
431
+ },
368
432
  close: () => {
369
- store.set(false);
370
- void client?.flush();
433
+ store.setOpen(false);
434
+ void clientRef?.flush();
435
+ },
436
+ client: clientRef,
437
+ get tosVersion() {
438
+ return clientRef.tosVersion;
371
439
  },
372
- client
440
+ get hasAcceptedTos() {
441
+ return clientRef.hasAcceptedTos;
442
+ },
443
+ acceptTos: async () => {
444
+ await clientRef.acceptTos();
445
+ if (!options.apiKey) await refreshState();
446
+ },
447
+ get isActive() {
448
+ if (cta === "show") return true;
449
+ if (cta === "hide") return false;
450
+ return addonState ? addonState.isActive : null;
451
+ },
452
+ get enabledLanguages() {
453
+ return addonState ? addonState.enabledLanguages : null;
454
+ },
455
+ get sku() {
456
+ return addonState ? addonState.sku : null;
457
+ }
373
458
  };
374
459
  options.onReady?.(controller);
375
460
  if (options.controllerRef) options.controllerRef.current = controller;
376
461
  return () => {
462
+ langUnsub?.();
377
463
  if (options.controllerRef) options.controllerRef.current = null;
378
464
  void client?.flush();
379
465
  };