@verbumia/feedback 0.2.6 → 0.2.8
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/dist/chunk-EKU6NNWI.js +40 -0
- package/dist/chunk-EKU6NNWI.js.map +1 -0
- package/dist/{chunk-5RTFWOGT.js → chunk-EMGN6MR4.js} +87 -3
- package/dist/chunk-EMGN6MR4.js.map +1 -0
- package/dist/{client-qgDSbz3A.d.cts → client-CnEK_2SD.d.cts} +114 -1
- package/dist/{client-qgDSbz3A.d.ts → client-CnEK_2SD.d.ts} +114 -1
- package/dist/core/index.cjs +86 -2
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/{keys-BhuK_fy1.d.cts → keys-2_T5bDpX.d.cts} +1 -1
- package/dist/{keys-CEWu0Htb.d.ts → keys-eHc_lx5v.d.ts} +1 -1
- package/dist/native/index.cjs +317 -48
- package/dist/native/index.cjs.map +1 -1
- package/dist/native/index.d.cts +48 -4
- package/dist/native/index.d.ts +48 -4
- package/dist/native/index.js +200 -48
- package/dist/native/index.js.map +1 -1
- package/dist/react/index.cjs +319 -41
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +119 -4
- package/dist/react/index.d.ts +119 -4
- package/dist/react/index.js +202 -41
- package/dist/react/index.js.map +1 -1
- package/dist/svelte/index.cjs +86 -2
- package/dist/svelte/index.cjs.map +1 -1
- package/dist/svelte/index.d.cts +2 -2
- package/dist/svelte/index.d.ts +2 -2
- package/dist/svelte/index.js +1 -1
- package/dist/vue/index.cjs +86 -2
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.d.cts +2 -2
- package/dist/vue/index.d.ts +2 -2
- package/dist/vue/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-5RTFWOGT.js.map +0 -1
package/dist/native/index.cjs
CHANGED
|
@@ -80,11 +80,61 @@ var FeedbackClient = class {
|
|
|
80
80
|
get hasConsented() {
|
|
81
81
|
return this.tokens !== null;
|
|
82
82
|
}
|
|
83
|
+
/** Alias of `hasConsented` exposed under the 0.2.7 naming used by the
|
|
84
|
+
* `controller.hasAcceptedTos` getter (the SDK's built-in modal and a
|
|
85
|
+
* host's external ToS page share the same persisted token bundle, so
|
|
86
|
+
* both flip this boolean once a session is bootstrapped). */
|
|
87
|
+
get hasAcceptedTos() {
|
|
88
|
+
return this.tokens !== null;
|
|
89
|
+
}
|
|
83
90
|
/** Server-minted sessionId / grouping_key (from the token bundle).
|
|
84
91
|
* Available only after `acceptTos()`; never client-generated. */
|
|
85
92
|
get sessionId() {
|
|
86
93
|
return this.tokens?.grouping_key;
|
|
87
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* 0.2.7 — `GET /v1/projects/{projectId}/feedback-addon/state`.
|
|
97
|
+
*
|
|
98
|
+
* Auth selection (matches backend's dual-acceptance after task 836's
|
|
99
|
+
* follow-up PR — see CONTRACT note in the response type):
|
|
100
|
+
* - If `cfg.apiKey` is set → `Authorization: ApiKey <key>` (so the
|
|
101
|
+
* plugin can fetch state at `setup()` time, before the user has
|
|
102
|
+
* accepted ToS).
|
|
103
|
+
* - Else if a user-session bundle exists → `Authorization: Bearer
|
|
104
|
+
* <access_token>` (the deferred-after-acceptTos path).
|
|
105
|
+
* - Else → throw `FeedbackError("addon state requires apiKey or
|
|
106
|
+
* acceptTos")`. The plugin's setup catches and surfaces a
|
|
107
|
+
* console.warn the first time, then re-attempts on the
|
|
108
|
+
* bundle-mint that follows `acceptTos()`.
|
|
109
|
+
*
|
|
110
|
+
* Best-effort: a transport error returns `null` instead of throwing,
|
|
111
|
+
* so the controller's `isActive` falls back to whatever was last
|
|
112
|
+
* known (or its initial value) without flipping the host's CTA into
|
|
113
|
+
* an inconsistent state on a transient blip.
|
|
114
|
+
*/
|
|
115
|
+
async getAddonState() {
|
|
116
|
+
let auth;
|
|
117
|
+
if (this.cfg.apiKey) {
|
|
118
|
+
auth = `ApiKey ${this.cfg.apiKey}`;
|
|
119
|
+
} else if (this.tokens) {
|
|
120
|
+
auth = `Bearer ${this.tokens.access_token}`;
|
|
121
|
+
} else {
|
|
122
|
+
throw new FeedbackError(
|
|
123
|
+
"addon state requires apiKey or acceptTos"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const url = `${this.base()}/v1/projects/${this.cfg.projectId}/feedback-addon/state`;
|
|
127
|
+
try {
|
|
128
|
+
const res = await this.fetchImpl(url, {
|
|
129
|
+
method: "GET",
|
|
130
|
+
headers: { Authorization: auth }
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok) return null;
|
|
133
|
+
return await res.json();
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
88
138
|
/** BCP-47 language the widget is rating strings in. */
|
|
89
139
|
get language() {
|
|
90
140
|
return this.cfg.language;
|
|
@@ -156,9 +206,18 @@ var FeedbackClient = class {
|
|
|
156
206
|
}
|
|
157
207
|
this.tokens = await res.json();
|
|
158
208
|
}
|
|
159
|
-
/** Authenticated fetch with a single transparent refresh-on-401 retry.
|
|
209
|
+
/** Authenticated fetch with a single transparent refresh-on-401 retry.
|
|
210
|
+
* When `cfg.autoAcceptTos === false` (the plugin's `tos: "skip"` opt-in
|
|
211
|
+
* / 0.2.7) and no token has been minted yet, throws a `not consented`
|
|
212
|
+
* error instead of silently calling `acceptTos()` — the host promises
|
|
213
|
+
* to handle consent externally via `controller.acceptTos()`. */
|
|
160
214
|
async authed(path, init, retry = true) {
|
|
161
|
-
if (!this.tokens)
|
|
215
|
+
if (!this.tokens) {
|
|
216
|
+
if (this.cfg.autoAcceptTos === false) {
|
|
217
|
+
throw new FeedbackError("not consented");
|
|
218
|
+
}
|
|
219
|
+
await this.acceptTos();
|
|
220
|
+
}
|
|
162
221
|
const res = await this.fetchImpl(`${this.base()}${path}`, {
|
|
163
222
|
...init,
|
|
164
223
|
headers: {
|
|
@@ -195,6 +254,31 @@ var FeedbackClient = class {
|
|
|
195
254
|
suggest(payload) {
|
|
196
255
|
this.enqueue({ kind: "suggestion", payload });
|
|
197
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* 0.2.8 — `PATCH /v1/feedback/suggestions/{id}` (backend task 847,
|
|
259
|
+
* deploy `45190c8`). Used by the panel's edit-mode editor when the
|
|
260
|
+
* end-user already has a pending suggestion for a string (the
|
|
261
|
+
* `FeedbackString.my_suggestion.id` from a prior submit). Submits
|
|
262
|
+
* synchronously rather than going through the rating/suggestion
|
|
263
|
+
* batch queue — the host triggered an explicit edit, not a passive
|
|
264
|
+
* rating, so we want the round-trip to surface before the panel
|
|
265
|
+
* closes. Best-effort failures bubble; the caller catches and
|
|
266
|
+
* shows an error row in the panel.
|
|
267
|
+
*
|
|
268
|
+
* Wire body: `{ text: string }` (backend probe confirmed). NOT the
|
|
269
|
+
* legacy `{ suggested_text }` of the batched POST path.
|
|
270
|
+
*/
|
|
271
|
+
async editSuggestion(id, text) {
|
|
272
|
+
if (!id) throw new FeedbackError("editSuggestion: id is required");
|
|
273
|
+
const trimmed = (text ?? "").trim();
|
|
274
|
+
if (!trimmed) throw new FeedbackError("editSuggestion: text is required");
|
|
275
|
+
const res = await this.authed(`/v1/feedback/suggestions/${encodeURIComponent(id)}`, {
|
|
276
|
+
method: "PATCH",
|
|
277
|
+
headers: { "Content-Type": "application/json" },
|
|
278
|
+
body: JSON.stringify({ text: trimmed })
|
|
279
|
+
});
|
|
280
|
+
if (!res.ok) throw await this.problem(res, "failed to update suggestion");
|
|
281
|
+
}
|
|
198
282
|
enqueue(item) {
|
|
199
283
|
this.queue.push(item);
|
|
200
284
|
if (this.queue.length >= this.cfg.maxBatch) {
|
|
@@ -291,6 +375,42 @@ function dedupe(keys) {
|
|
|
291
375
|
return out;
|
|
292
376
|
}
|
|
293
377
|
|
|
378
|
+
// src/core/locales.ts
|
|
379
|
+
var MESSAGES = {
|
|
380
|
+
en: {
|
|
381
|
+
showOriginal: "Show original",
|
|
382
|
+
submitSuggestion: "Submit a suggestion",
|
|
383
|
+
updateMySuggestion: "Update my suggestion"
|
|
384
|
+
},
|
|
385
|
+
fr: {
|
|
386
|
+
showOriginal: "Voir l'original",
|
|
387
|
+
submitSuggestion: "Soumettre une suggestion",
|
|
388
|
+
updateMySuggestion: "Mettre \xE0 jour ma suggestion"
|
|
389
|
+
},
|
|
390
|
+
es: {
|
|
391
|
+
showOriginal: "Ver el original",
|
|
392
|
+
submitSuggestion: "Enviar una sugerencia",
|
|
393
|
+
updateMySuggestion: "Actualizar mi sugerencia"
|
|
394
|
+
},
|
|
395
|
+
de: {
|
|
396
|
+
showOriginal: "Original anzeigen",
|
|
397
|
+
submitSuggestion: "Vorschlag einreichen",
|
|
398
|
+
updateMySuggestion: "Meinen Vorschlag aktualisieren"
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
function t(locale, key) {
|
|
402
|
+
if (locale) {
|
|
403
|
+
const exact = MESSAGES[locale];
|
|
404
|
+
if (exact) return exact[key];
|
|
405
|
+
const base = locale.split("-")[0];
|
|
406
|
+
if (base) {
|
|
407
|
+
const baseMsgs = MESSAGES[base];
|
|
408
|
+
if (baseMsgs) return baseMsgs[key];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return MESSAGES.en[key];
|
|
412
|
+
}
|
|
413
|
+
|
|
294
414
|
// src/native/panel.tsx
|
|
295
415
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
296
416
|
var SafeAreaView;
|
|
@@ -314,11 +434,14 @@ var C = {
|
|
|
314
434
|
emeraldSoft: "#34d399"
|
|
315
435
|
};
|
|
316
436
|
function FeedbackModal(props) {
|
|
317
|
-
const { client, visible, keys, namespace, onClose } = props;
|
|
318
|
-
const [consented, setConsented] = (0, import_react.useState)(
|
|
437
|
+
const { client, visible, keys, namespace, tos = "modal", onClose } = props;
|
|
438
|
+
const [consented, setConsented] = (0, import_react.useState)(
|
|
439
|
+
tos === "skip" ? true : client.hasConsented
|
|
440
|
+
);
|
|
319
441
|
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
320
442
|
const [error, setError] = (0, import_react.useState)(null);
|
|
321
443
|
const [strings, setStrings] = (0, import_react.useState)([]);
|
|
444
|
+
const [showSource, setShowSource] = (0, import_react.useState)(false);
|
|
322
445
|
const loadStrings = (0, import_react.useCallback)(async () => {
|
|
323
446
|
setBusy(true);
|
|
324
447
|
setError(null);
|
|
@@ -416,14 +539,49 @@ function FeedbackModal(props) {
|
|
|
416
539
|
// to its intrinsic height and the panel left a large empty
|
|
417
540
|
// area below the last row (the second half of the
|
|
418
541
|
// SeedSower repro).
|
|
419
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.ScrollView, { style: { flex: 1 }, children: !strings.length ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: C.dim }, children: busy ? "Loading\u2026" : "No strings to review on this view." }) :
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
542
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.ScrollView, { style: { flex: 1 }, children: !strings.length ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: C.dim }, children: busy ? "Loading\u2026" : "No strings to review on this view." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
543
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
544
|
+
import_react_native.TouchableOpacity,
|
|
545
|
+
{
|
|
546
|
+
onPress: () => setShowSource((v) => !v),
|
|
547
|
+
accessibilityLabel: t(client.language, "showOriginal"),
|
|
548
|
+
style: {
|
|
549
|
+
alignSelf: "flex-end",
|
|
550
|
+
flexDirection: "row",
|
|
551
|
+
alignItems: "center",
|
|
552
|
+
marginBottom: 10,
|
|
553
|
+
paddingVertical: 4,
|
|
554
|
+
paddingHorizontal: 8,
|
|
555
|
+
borderWidth: 1,
|
|
556
|
+
borderColor: C.border,
|
|
557
|
+
borderRadius: 6
|
|
558
|
+
},
|
|
559
|
+
children: [
|
|
560
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
561
|
+
import_react_native.Text,
|
|
562
|
+
{
|
|
563
|
+
style: {
|
|
564
|
+
color: showSource ? C.emeraldSoft : C.dim,
|
|
565
|
+
fontSize: 12,
|
|
566
|
+
marginRight: 6
|
|
567
|
+
},
|
|
568
|
+
children: showSource ? "\u2611" : "\u2610"
|
|
569
|
+
}
|
|
570
|
+
),
|
|
571
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: C.dim, fontSize: 12 }, children: t(client.language, "showOriginal") })
|
|
572
|
+
]
|
|
573
|
+
}
|
|
574
|
+
),
|
|
575
|
+
strings.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
576
|
+
StringRow,
|
|
577
|
+
{
|
|
578
|
+
s,
|
|
579
|
+
client,
|
|
580
|
+
showSource
|
|
581
|
+
},
|
|
582
|
+
`${s.namespace}:${s.key}`
|
|
583
|
+
))
|
|
584
|
+
] }) })
|
|
427
585
|
)
|
|
428
586
|
] })
|
|
429
587
|
}
|
|
@@ -434,11 +592,42 @@ function FeedbackModal(props) {
|
|
|
434
592
|
);
|
|
435
593
|
}
|
|
436
594
|
function StringRow(props) {
|
|
437
|
-
const { s, client } = props;
|
|
595
|
+
const { s, client, showSource } = props;
|
|
438
596
|
const [mine, setMine] = (0, import_react.useState)(s.my_rating);
|
|
439
597
|
const [show, setShow] = (0, import_react.useState)(false);
|
|
440
|
-
const
|
|
598
|
+
const mySuggestionId = s.my_suggestion ? s.my_suggestion.id : null;
|
|
599
|
+
const [text, setText] = (0, import_react.useState)(s.my_suggestion?.text ?? "");
|
|
441
600
|
const [sent, setSent] = (0, import_react.useState)(false);
|
|
601
|
+
const [editError, setEditError] = (0, import_react.useState)(null);
|
|
602
|
+
const submit = async () => {
|
|
603
|
+
const trimmed = text.trim();
|
|
604
|
+
if (!trimmed) return;
|
|
605
|
+
setEditError(null);
|
|
606
|
+
try {
|
|
607
|
+
if (mySuggestionId) {
|
|
608
|
+
await client.editSuggestion(mySuggestionId, trimmed);
|
|
609
|
+
} else {
|
|
610
|
+
client.suggest({
|
|
611
|
+
namespace: s.namespace,
|
|
612
|
+
key: s.key,
|
|
613
|
+
language: client.language,
|
|
614
|
+
translation_hash: s.translation_hash,
|
|
615
|
+
suggested_text: trimmed
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
setSent(true);
|
|
619
|
+
setShow(false);
|
|
620
|
+
} catch (e) {
|
|
621
|
+
setEditError(
|
|
622
|
+
e instanceof Error ? e.message : "Could not update the suggestion"
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
const showSourceRow = showSource && s.source_locale !== client.language;
|
|
627
|
+
const submitLabel = t(
|
|
628
|
+
client.language,
|
|
629
|
+
mySuggestionId ? "updateMySuggestion" : "submitSuggestion"
|
|
630
|
+
);
|
|
442
631
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
443
632
|
import_react_native.View,
|
|
444
633
|
{
|
|
@@ -456,6 +645,19 @@ function StringRow(props) {
|
|
|
456
645
|
" \xB7 ",
|
|
457
646
|
s.key
|
|
458
647
|
] }),
|
|
648
|
+
showSourceRow ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
649
|
+
import_react_native.Text,
|
|
650
|
+
{
|
|
651
|
+
accessibilityLabel: "source",
|
|
652
|
+
style: {
|
|
653
|
+
color: C.dim,
|
|
654
|
+
fontSize: 12,
|
|
655
|
+
fontStyle: "italic",
|
|
656
|
+
marginTop: 4
|
|
657
|
+
},
|
|
658
|
+
children: s.source_text
|
|
659
|
+
}
|
|
660
|
+
) : null,
|
|
459
661
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: C.text, fontSize: 15, marginVertical: 6 }, children: s.value }),
|
|
460
662
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: { flexDirection: "row" }, children: [
|
|
461
663
|
[1, 2, 3, 4, 5].map((n) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -491,7 +693,7 @@ function StringRow(props) {
|
|
|
491
693
|
{
|
|
492
694
|
onPress: () => setShow(!show),
|
|
493
695
|
style: { marginLeft: "auto" },
|
|
494
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: C.emeraldSoft }, children: sent ? "Suggested \u2713" :
|
|
696
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: C.emeraldSoft }, children: sent ? "Suggested \u2713" : submitLabel })
|
|
495
697
|
}
|
|
496
698
|
)
|
|
497
699
|
] }),
|
|
@@ -515,29 +717,18 @@ function StringRow(props) {
|
|
|
515
717
|
}
|
|
516
718
|
}
|
|
517
719
|
),
|
|
720
|
+
editError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: "#f87171", fontSize: 12, marginTop: 6 }, children: editError }) : null,
|
|
518
721
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
519
722
|
import_react_native.TouchableOpacity,
|
|
520
723
|
{
|
|
521
|
-
onPress: () =>
|
|
522
|
-
if (!text.trim()) return;
|
|
523
|
-
client.suggest({
|
|
524
|
-
namespace: s.namespace,
|
|
525
|
-
key: s.key,
|
|
526
|
-
language: client.language,
|
|
527
|
-
translation_hash: s.translation_hash,
|
|
528
|
-
suggested_text: text.trim()
|
|
529
|
-
});
|
|
530
|
-
setSent(true);
|
|
531
|
-
setShow(false);
|
|
532
|
-
setText("");
|
|
533
|
-
},
|
|
724
|
+
onPress: () => void submit(),
|
|
534
725
|
style: {
|
|
535
726
|
marginTop: 6,
|
|
536
727
|
backgroundColor: C.emerald,
|
|
537
728
|
borderRadius: 6,
|
|
538
729
|
padding: 10
|
|
539
730
|
},
|
|
540
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: "#03110c", fontWeight: "700", textAlign: "center" }, children:
|
|
731
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: "#03110c", fontWeight: "700", textAlign: "center" }, children: submitLabel })
|
|
541
732
|
}
|
|
542
733
|
)
|
|
543
734
|
] }) : null
|
|
@@ -549,42 +740,73 @@ function StringRow(props) {
|
|
|
549
740
|
// src/native/plugin.tsx
|
|
550
741
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
551
742
|
function makeStore() {
|
|
552
|
-
let
|
|
743
|
+
let state = { isOpen: false, snapshotKeys: void 0 };
|
|
553
744
|
const listeners = /* @__PURE__ */ new Set();
|
|
554
745
|
return {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
open
|
|
559
|
-
|
|
746
|
+
getState: () => state,
|
|
747
|
+
setOpen(open, snapshotKeys) {
|
|
748
|
+
const next = {
|
|
749
|
+
isOpen: open,
|
|
750
|
+
snapshotKeys: open ? snapshotKeys : void 0
|
|
751
|
+
};
|
|
752
|
+
if (state.isOpen === next.isOpen && state.snapshotKeys === next.snapshotKeys) {
|
|
753
|
+
return;
|
|
560
754
|
}
|
|
755
|
+
state = next;
|
|
756
|
+
listeners.forEach((l) => l());
|
|
561
757
|
},
|
|
562
758
|
subscribe(l) {
|
|
563
759
|
listeners.add(l);
|
|
564
|
-
return () =>
|
|
760
|
+
return () => {
|
|
761
|
+
listeners.delete(l);
|
|
762
|
+
};
|
|
565
763
|
}
|
|
566
764
|
};
|
|
567
765
|
}
|
|
766
|
+
async function captureCurrentViewSnapshot(i18next) {
|
|
767
|
+
const reg = globalThis.__verbumia_key_registry__;
|
|
768
|
+
if (!reg) return [];
|
|
769
|
+
const lng = i18next?.language;
|
|
770
|
+
const change = i18next?.changeLanguage;
|
|
771
|
+
if (typeof change === "function" && typeof lng === "string" && lng) {
|
|
772
|
+
reg.reset?.();
|
|
773
|
+
try {
|
|
774
|
+
await change(lng);
|
|
775
|
+
} catch {
|
|
776
|
+
}
|
|
777
|
+
await new Promise((r) => {
|
|
778
|
+
if (typeof requestAnimationFrame === "function") {
|
|
779
|
+
requestAnimationFrame(() => r());
|
|
780
|
+
} else {
|
|
781
|
+
setTimeout(() => r(), 16);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
return reg.snapshot();
|
|
785
|
+
}
|
|
786
|
+
return reg.snapshot();
|
|
787
|
+
}
|
|
568
788
|
function feedbackPlugin(options) {
|
|
569
789
|
const store = makeStore();
|
|
570
790
|
let client = null;
|
|
571
791
|
function Outlet() {
|
|
572
|
-
const
|
|
792
|
+
const state = (0, import_react2.useSyncExternalStore)(
|
|
573
793
|
store.subscribe,
|
|
574
|
-
store.
|
|
575
|
-
store.
|
|
794
|
+
store.getState,
|
|
795
|
+
store.getState
|
|
576
796
|
);
|
|
577
797
|
if (!client) return null;
|
|
578
798
|
const c = client;
|
|
799
|
+
const panelKeys = options.keys ?? state.snapshotKeys;
|
|
579
800
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
580
801
|
FeedbackModal,
|
|
581
802
|
{
|
|
582
803
|
client: c,
|
|
583
|
-
visible: isOpen,
|
|
584
|
-
keys:
|
|
804
|
+
visible: state.isOpen,
|
|
805
|
+
keys: panelKeys,
|
|
585
806
|
namespace: options.namespace,
|
|
807
|
+
tos: options.tos ?? "modal",
|
|
586
808
|
onClose: () => {
|
|
587
|
-
store.
|
|
809
|
+
store.setOpen(false);
|
|
588
810
|
void c.flush();
|
|
589
811
|
}
|
|
590
812
|
}
|
|
@@ -594,24 +816,71 @@ function feedbackPlugin(options) {
|
|
|
594
816
|
name: "@verbumia/feedback",
|
|
595
817
|
setup(ctx) {
|
|
596
818
|
const initialLanguage = options.language ?? ctx.i18n?.language ?? ctx.config.defaultLocale;
|
|
819
|
+
const tos = options.tos ?? "modal";
|
|
597
820
|
client = new FeedbackClient({
|
|
598
821
|
apiBase: options.apiBase ?? ctx.config.apiBase ?? "https://api.verbumia.dev",
|
|
599
822
|
projectId: options.projectId ?? ctx.config.projectUuid,
|
|
600
823
|
language: initialLanguage,
|
|
601
824
|
endUserId: options.endUserId,
|
|
602
|
-
fetchImpl: options.fetchImpl
|
|
825
|
+
fetchImpl: options.fetchImpl,
|
|
826
|
+
apiKey: options.apiKey,
|
|
827
|
+
autoAcceptTos: tos !== "skip"
|
|
603
828
|
});
|
|
829
|
+
const clientRef = client;
|
|
830
|
+
let addonState = null;
|
|
831
|
+
const cta = options.cta ?? "auto";
|
|
832
|
+
const scope = options.scope ?? "current-view";
|
|
833
|
+
const ctxI18next = ctx.i18n?.i18next;
|
|
834
|
+
const refreshState = async () => {
|
|
835
|
+
try {
|
|
836
|
+
const next = await clientRef.getAddonState();
|
|
837
|
+
if (next !== null) addonState = next;
|
|
838
|
+
} catch {
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
if (options.apiKey) void refreshState();
|
|
604
842
|
let langUnsub;
|
|
605
843
|
if (typeof ctx.onLanguageChange === "function") {
|
|
606
|
-
langUnsub = ctx.onLanguageChange((lng) =>
|
|
844
|
+
langUnsub = ctx.onLanguageChange((lng) => {
|
|
845
|
+
clientRef.setLanguage(lng);
|
|
846
|
+
void refreshState();
|
|
847
|
+
});
|
|
607
848
|
}
|
|
608
849
|
const controller = {
|
|
609
|
-
open: () =>
|
|
850
|
+
open: async () => {
|
|
851
|
+
if (scope === "current-view") {
|
|
852
|
+
const snapshot = await captureCurrentViewSnapshot(ctxI18next);
|
|
853
|
+
store.setOpen(true, snapshot);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
store.setOpen(true);
|
|
857
|
+
},
|
|
610
858
|
close: () => {
|
|
611
|
-
store.
|
|
612
|
-
void
|
|
859
|
+
store.setOpen(false);
|
|
860
|
+
void clientRef?.flush();
|
|
861
|
+
},
|
|
862
|
+
client: clientRef,
|
|
863
|
+
get tosVersion() {
|
|
864
|
+
return clientRef.tosVersion;
|
|
865
|
+
},
|
|
866
|
+
get hasAcceptedTos() {
|
|
867
|
+
return clientRef.hasAcceptedTos;
|
|
868
|
+
},
|
|
869
|
+
acceptTos: async () => {
|
|
870
|
+
await clientRef.acceptTos();
|
|
871
|
+
if (!options.apiKey) await refreshState();
|
|
613
872
|
},
|
|
614
|
-
|
|
873
|
+
get isActive() {
|
|
874
|
+
if (cta === "show") return true;
|
|
875
|
+
if (cta === "hide") return false;
|
|
876
|
+
return addonState ? addonState.isActive : null;
|
|
877
|
+
},
|
|
878
|
+
get enabledLanguages() {
|
|
879
|
+
return addonState ? addonState.enabledLanguages : null;
|
|
880
|
+
},
|
|
881
|
+
get sku() {
|
|
882
|
+
return addonState ? addonState.sku : null;
|
|
883
|
+
}
|
|
615
884
|
};
|
|
616
885
|
options.onReady?.(controller);
|
|
617
886
|
if (options.controllerRef) options.controllerRef.current = controller;
|