@verbumia/feedback 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/CONTRACT.md +165 -0
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/chunk-5NA2TFPG.js +1 -0
- package/dist/chunk-5NA2TFPG.js.map +1 -0
- package/dist/chunk-OX4RJD5H.js +242 -0
- package/dist/chunk-OX4RJD5H.js.map +1 -0
- package/dist/client-CPEcvn23.d.cts +159 -0
- package/dist/client-CPEcvn23.d.ts +159 -0
- package/dist/core/index.cjs +272 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +18 -0
- package/dist/core/index.d.ts +18 -0
- package/dist/core/index.js +16 -0
- package/dist/core/index.js.map +1 -0
- package/dist/keys-BySe1O6V.d.ts +25 -0
- package/dist/keys-Dg_nv16u.d.cts +25 -0
- package/dist/native/index.cjs +575 -0
- package/dist/native/index.cjs.map +1 -0
- package/dist/native/index.d.cts +54 -0
- package/dist/native/index.d.ts +54 -0
- package/dist/native/index.js +322 -0
- package/dist/native/index.js.map +1 -0
- package/dist/react/index.cjs +644 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +55 -0
- package/dist/react/index.d.ts +55 -0
- package/dist/react/index.js +384 -0
- package/dist/react/index.js.map +1 -0
- package/dist/svelte/index.cjs +306 -0
- package/dist/svelte/index.cjs.map +1 -0
- package/dist/svelte/index.d.cts +38 -0
- package/dist/svelte/index.d.ts +38 -0
- package/dist/svelte/index.js +52 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/vue/index.cjs +426 -0
- package/dist/vue/index.cjs.map +1 -0
- package/dist/vue/index.d.cts +39 -0
- package/dist/vue/index.d.ts +39 -0
- package/dist/vue/index.js +172 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +108 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "../chunk-5NA2TFPG.js";
|
|
2
|
+
import {
|
|
3
|
+
FeedbackClient,
|
|
4
|
+
FeedbackError,
|
|
5
|
+
SDK_TOS_VERSION,
|
|
6
|
+
hasKeyRegistry,
|
|
7
|
+
resolveKeys
|
|
8
|
+
} from "../chunk-OX4RJD5H.js";
|
|
9
|
+
export {
|
|
10
|
+
FeedbackClient,
|
|
11
|
+
FeedbackError,
|
|
12
|
+
SDK_TOS_VERSION,
|
|
13
|
+
hasKeyRegistry,
|
|
14
|
+
resolveKeys
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { D as DeclaredKey } from './client-CPEcvn23.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* On-screen key discovery.
|
|
5
|
+
*
|
|
6
|
+
* Preferred source: the `@verbumia/*-i18n` SDK exposes a lightweight key
|
|
7
|
+
* registry of keys it has rendered. When that registry is present on the
|
|
8
|
+
* global (the i18n SDK publishes `globalThis.__verbumia_key_registry__`),
|
|
9
|
+
* we read the keys touched on the current view. Otherwise the host app
|
|
10
|
+
* passes an explicit `keys` list — the always-available fallback.
|
|
11
|
+
*
|
|
12
|
+
* The registry contract is intentionally tiny so any framework port of the
|
|
13
|
+
* i18n SDK can implement it without depending on this package.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** True when a compatible `@verbumia/*-i18n` registry is detectable. */
|
|
17
|
+
declare function hasKeyRegistry(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the on-screen keys: explicit `keys` prop always wins (it is the
|
|
20
|
+
* customer's authoritative declaration); otherwise fall back to the i18n
|
|
21
|
+
* registry snapshot; otherwise an empty list (widget shows "no strings").
|
|
22
|
+
*/
|
|
23
|
+
declare function resolveKeys(explicit?: DeclaredKey[]): DeclaredKey[];
|
|
24
|
+
|
|
25
|
+
export { hasKeyRegistry as h, resolveKeys as r };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { D as DeclaredKey } from './client-CPEcvn23.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* On-screen key discovery.
|
|
5
|
+
*
|
|
6
|
+
* Preferred source: the `@verbumia/*-i18n` SDK exposes a lightweight key
|
|
7
|
+
* registry of keys it has rendered. When that registry is present on the
|
|
8
|
+
* global (the i18n SDK publishes `globalThis.__verbumia_key_registry__`),
|
|
9
|
+
* we read the keys touched on the current view. Otherwise the host app
|
|
10
|
+
* passes an explicit `keys` list — the always-available fallback.
|
|
11
|
+
*
|
|
12
|
+
* The registry contract is intentionally tiny so any framework port of the
|
|
13
|
+
* i18n SDK can implement it without depending on this package.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** True when a compatible `@verbumia/*-i18n` registry is detectable. */
|
|
17
|
+
declare function hasKeyRegistry(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the on-screen keys: explicit `keys` prop always wins (it is the
|
|
20
|
+
* customer's authoritative declaration); otherwise fall back to the i18n
|
|
21
|
+
* registry snapshot; otherwise an empty list (widget shows "no strings").
|
|
22
|
+
*/
|
|
23
|
+
declare function resolveKeys(explicit?: DeclaredKey[]): DeclaredKey[];
|
|
24
|
+
|
|
25
|
+
export { hasKeyRegistry as h, resolveKeys as r };
|
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/native/index.ts
|
|
21
|
+
var native_exports = {};
|
|
22
|
+
__export(native_exports, {
|
|
23
|
+
FeedbackClient: () => FeedbackClient,
|
|
24
|
+
FeedbackError: () => FeedbackError,
|
|
25
|
+
FeedbackModal: () => FeedbackModal,
|
|
26
|
+
feedbackPlugin: () => feedbackPlugin,
|
|
27
|
+
hasKeyRegistry: () => hasKeyRegistry,
|
|
28
|
+
resolveKeys: () => resolveKeys
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(native_exports);
|
|
31
|
+
|
|
32
|
+
// src/native/plugin.tsx
|
|
33
|
+
var import_react2 = require("react");
|
|
34
|
+
|
|
35
|
+
// src/core/types.ts
|
|
36
|
+
var FeedbackError = class extends Error {
|
|
37
|
+
constructor(message, status, code) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.status = status;
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.name = "FeedbackError";
|
|
42
|
+
}
|
|
43
|
+
status;
|
|
44
|
+
code;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/core/tos.ts
|
|
48
|
+
var SDK_TOS_VERSION = "2026-05-18";
|
|
49
|
+
|
|
50
|
+
// src/core/client.ts
|
|
51
|
+
var SDK_LIB = "@verbumia/feedback";
|
|
52
|
+
var SDK_VER = "0.1.0";
|
|
53
|
+
var FeedbackClient = class {
|
|
54
|
+
cfg;
|
|
55
|
+
fetchImpl;
|
|
56
|
+
tokens = null;
|
|
57
|
+
queue = [];
|
|
58
|
+
timer = null;
|
|
59
|
+
bootstrapping = null;
|
|
60
|
+
constructor(config) {
|
|
61
|
+
this.cfg = {
|
|
62
|
+
flushDebounceMs: 1500,
|
|
63
|
+
maxBatch: 20,
|
|
64
|
+
...config
|
|
65
|
+
};
|
|
66
|
+
const f = config.fetchImpl ?? globalThis.fetch;
|
|
67
|
+
if (!f) {
|
|
68
|
+
throw new FeedbackError(
|
|
69
|
+
"no fetch implementation available; pass config.fetchImpl"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
this.fetchImpl = f.bind(globalThis);
|
|
73
|
+
}
|
|
74
|
+
base() {
|
|
75
|
+
return this.cfg.apiBase.replace(/\/+$/, "");
|
|
76
|
+
}
|
|
77
|
+
get endUserId() {
|
|
78
|
+
return this.tokens?.end_user_id ?? this.cfg.endUserId;
|
|
79
|
+
}
|
|
80
|
+
get hasConsented() {
|
|
81
|
+
return this.tokens !== null;
|
|
82
|
+
}
|
|
83
|
+
/** Server-minted sessionId / grouping_key (from the token bundle).
|
|
84
|
+
* Available only after `acceptTos()`; never client-generated. */
|
|
85
|
+
get sessionId() {
|
|
86
|
+
return this.tokens?.grouping_key;
|
|
87
|
+
}
|
|
88
|
+
/** BCP-47 language the widget is rating strings in. */
|
|
89
|
+
get language() {
|
|
90
|
+
return this.cfg.language;
|
|
91
|
+
}
|
|
92
|
+
/** ToS version the end user is asked to accept — the SDK's
|
|
93
|
+
* build-time constant (task 616). NOT integrator/server set. */
|
|
94
|
+
get tosVersion() {
|
|
95
|
+
return SDK_TOS_VERSION;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Accept the ToS and bootstrap a scoped token. Idempotent: a second call
|
|
99
|
+
* returns the in-flight / existing bundle rather than re-accepting.
|
|
100
|
+
*/
|
|
101
|
+
async acceptTos() {
|
|
102
|
+
if (this.tokens) return this.tokens;
|
|
103
|
+
if (this.bootstrapping) return this.bootstrapping;
|
|
104
|
+
this.bootstrapping = (async () => {
|
|
105
|
+
const res = await this.fetchImpl(`${this.base()}/v1/feedback/tos`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "Content-Type": "application/json" },
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
// NO grouping_key: server-minted (task 599). It comes back in
|
|
110
|
+
// the token bundle and is bound into the JWT server-side.
|
|
111
|
+
project_id: this.cfg.projectId,
|
|
112
|
+
end_user_id: this.cfg.endUserId,
|
|
113
|
+
tos_version: SDK_TOS_VERSION,
|
|
114
|
+
locale: this.cfg.locale
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
if (!res.ok) throw await this.problem(res, "tos acceptance failed");
|
|
118
|
+
this.tokens = await res.json();
|
|
119
|
+
return this.tokens;
|
|
120
|
+
})();
|
|
121
|
+
try {
|
|
122
|
+
return await this.bootstrapping;
|
|
123
|
+
} finally {
|
|
124
|
+
this.bootstrapping = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async refresh() {
|
|
128
|
+
if (!this.tokens) throw new FeedbackError("not consented");
|
|
129
|
+
const res = await this.fetchImpl(
|
|
130
|
+
`${this.base()}/v1/feedback/token/refresh`,
|
|
131
|
+
{
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "Content-Type": "application/json" },
|
|
134
|
+
body: JSON.stringify({ refresh_token: this.tokens.refresh_token })
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
this.tokens = null;
|
|
139
|
+
throw await this.problem(res, "token refresh failed");
|
|
140
|
+
}
|
|
141
|
+
this.tokens = await res.json();
|
|
142
|
+
}
|
|
143
|
+
/** Authenticated fetch with a single transparent refresh-on-401 retry. */
|
|
144
|
+
async authed(path, init, retry = true) {
|
|
145
|
+
if (!this.tokens) await this.acceptTos();
|
|
146
|
+
const res = await this.fetchImpl(`${this.base()}${path}`, {
|
|
147
|
+
...init,
|
|
148
|
+
headers: {
|
|
149
|
+
...init.headers ?? {},
|
|
150
|
+
Authorization: `Bearer ${this.tokens.access_token}`
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
if (res.status === 401 && retry) {
|
|
154
|
+
await this.refresh();
|
|
155
|
+
return this.authed(path, init, false);
|
|
156
|
+
}
|
|
157
|
+
return res;
|
|
158
|
+
}
|
|
159
|
+
/** Strings rendered on the current view, with this end user's prior rating. */
|
|
160
|
+
async getStrings(opts) {
|
|
161
|
+
const qs = new URLSearchParams({ language: this.cfg.language });
|
|
162
|
+
if (opts?.keys?.length) {
|
|
163
|
+
qs.set("keys", opts.keys.map((k) => `${k.namespace}:${k.key}`).join(","));
|
|
164
|
+
}
|
|
165
|
+
if (opts?.namespace) qs.set("namespace", opts.namespace);
|
|
166
|
+
if (opts?.limit) qs.set("limit", String(opts.limit));
|
|
167
|
+
const res = await this.authed(`/v1/feedback/strings?${qs}`, {
|
|
168
|
+
method: "GET"
|
|
169
|
+
});
|
|
170
|
+
if (!res.ok) throw await this.problem(res, "failed to load strings");
|
|
171
|
+
return await res.json();
|
|
172
|
+
}
|
|
173
|
+
/** Queue a rating; flushed on debounce or when the batch fills. */
|
|
174
|
+
rate(payload) {
|
|
175
|
+
this.enqueue({ kind: "rating", payload });
|
|
176
|
+
}
|
|
177
|
+
/** Queue a suggestion; flushed on debounce or when the batch fills. */
|
|
178
|
+
suggest(payload) {
|
|
179
|
+
this.enqueue({ kind: "suggestion", payload });
|
|
180
|
+
}
|
|
181
|
+
enqueue(item) {
|
|
182
|
+
this.queue.push(item);
|
|
183
|
+
if (this.queue.length >= this.cfg.maxBatch) {
|
|
184
|
+
void this.flush();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (this.timer) clearTimeout(this.timer);
|
|
188
|
+
this.timer = setTimeout(() => void this.flush(), this.cfg.flushDebounceMs);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Flush queued items. Best-effort: a transport/auth failure re-queues the
|
|
192
|
+
* batch once and swallows the error so the host app never sees a throw.
|
|
193
|
+
*/
|
|
194
|
+
async flush() {
|
|
195
|
+
if (this.timer) {
|
|
196
|
+
clearTimeout(this.timer);
|
|
197
|
+
this.timer = null;
|
|
198
|
+
}
|
|
199
|
+
if (!this.queue.length) return;
|
|
200
|
+
const batch = this.queue;
|
|
201
|
+
this.queue = [];
|
|
202
|
+
const ratings = batch.filter((b) => b.kind === "rating").map((b) => b.payload);
|
|
203
|
+
const suggestions = batch.filter((b) => b.kind === "suggestion").map((b) => b.payload);
|
|
204
|
+
try {
|
|
205
|
+
if (ratings.length) {
|
|
206
|
+
await this.postBatch("/v1/feedback/ratings", { ratings });
|
|
207
|
+
}
|
|
208
|
+
if (suggestions.length) {
|
|
209
|
+
await this.postBatch("/v1/feedback/suggestions", { suggestions });
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
this.queue.unshift(...batch);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async postBatch(path, body) {
|
|
216
|
+
const res = await this.authed(path, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: { "Content-Type": "application/json", "X-SDK": `${SDK_LIB}@${SDK_VER}` },
|
|
219
|
+
body: JSON.stringify(body)
|
|
220
|
+
});
|
|
221
|
+
if (!res.ok) throw await this.problem(res, "batch post failed");
|
|
222
|
+
return await res.json();
|
|
223
|
+
}
|
|
224
|
+
async problem(res, fallback) {
|
|
225
|
+
let code;
|
|
226
|
+
let detail = fallback;
|
|
227
|
+
try {
|
|
228
|
+
const body = await res.json();
|
|
229
|
+
code = body.code;
|
|
230
|
+
if (body.detail) detail = body.detail;
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
return new FeedbackError(detail, res.status, code);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/native/panel.tsx
|
|
238
|
+
var import_react = require("react");
|
|
239
|
+
var import_react_native = require("react-native");
|
|
240
|
+
|
|
241
|
+
// src/core/keys.ts
|
|
242
|
+
var REGISTRY_GLOBAL = "__verbumia_key_registry__";
|
|
243
|
+
function getRegistry() {
|
|
244
|
+
const g = globalThis;
|
|
245
|
+
const reg = g[REGISTRY_GLOBAL];
|
|
246
|
+
if (reg && typeof reg.snapshot === "function") {
|
|
247
|
+
return reg;
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
function hasKeyRegistry() {
|
|
252
|
+
return getRegistry() !== null;
|
|
253
|
+
}
|
|
254
|
+
function resolveKeys(explicit) {
|
|
255
|
+
if (explicit && explicit.length) return dedupe(explicit);
|
|
256
|
+
const reg = getRegistry();
|
|
257
|
+
if (reg) return dedupe(reg.snapshot());
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
function dedupe(keys) {
|
|
261
|
+
const seen = /* @__PURE__ */ new Set();
|
|
262
|
+
const out = [];
|
|
263
|
+
for (const k of keys) {
|
|
264
|
+
const id = `${k.namespace}:${k.key}`;
|
|
265
|
+
if (!seen.has(id)) {
|
|
266
|
+
seen.add(id);
|
|
267
|
+
out.push(k);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/native/panel.tsx
|
|
274
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
275
|
+
var C = {
|
|
276
|
+
bg: "#0b0f0e",
|
|
277
|
+
panel: "#111714",
|
|
278
|
+
border: "#1f2a25",
|
|
279
|
+
text: "#e7f5ef",
|
|
280
|
+
dim: "#8aa79b",
|
|
281
|
+
emerald: "#10b981",
|
|
282
|
+
emeraldSoft: "#34d399"
|
|
283
|
+
};
|
|
284
|
+
function FeedbackModal(props) {
|
|
285
|
+
const { client, visible, keys, onClose } = props;
|
|
286
|
+
const [consented, setConsented] = (0, import_react.useState)(client.hasConsented);
|
|
287
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
288
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
289
|
+
const [strings, setStrings] = (0, import_react.useState)([]);
|
|
290
|
+
const loadStrings = (0, import_react.useCallback)(async () => {
|
|
291
|
+
setBusy(true);
|
|
292
|
+
setError(null);
|
|
293
|
+
try {
|
|
294
|
+
const resolved = resolveKeys(keys);
|
|
295
|
+
const res = await client.getStrings({
|
|
296
|
+
keys: resolved.length ? resolved : void 0
|
|
297
|
+
});
|
|
298
|
+
setStrings(res.strings);
|
|
299
|
+
} catch (e) {
|
|
300
|
+
setError(e instanceof Error ? e.message : "Failed to load strings");
|
|
301
|
+
} finally {
|
|
302
|
+
setBusy(false);
|
|
303
|
+
}
|
|
304
|
+
}, [client, keys]);
|
|
305
|
+
(0, import_react.useEffect)(() => {
|
|
306
|
+
if (visible && consented) void loadStrings();
|
|
307
|
+
}, [visible, consented, loadStrings]);
|
|
308
|
+
const accept = (0, import_react.useCallback)(async () => {
|
|
309
|
+
setBusy(true);
|
|
310
|
+
try {
|
|
311
|
+
await client.acceptTos();
|
|
312
|
+
setConsented(true);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
setError(e instanceof Error ? e.message : "Could not accept the terms");
|
|
315
|
+
} finally {
|
|
316
|
+
setBusy(false);
|
|
317
|
+
}
|
|
318
|
+
}, [client]);
|
|
319
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
320
|
+
import_react_native.Modal,
|
|
321
|
+
{
|
|
322
|
+
visible,
|
|
323
|
+
transparent: true,
|
|
324
|
+
animationType: "slide",
|
|
325
|
+
onRequestClose: onClose,
|
|
326
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: { flex: 1, backgroundColor: "rgba(0,0,0,.55)", justifyContent: "flex-end" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
327
|
+
import_react_native.View,
|
|
328
|
+
{
|
|
329
|
+
style: {
|
|
330
|
+
maxHeight: "85%",
|
|
331
|
+
backgroundColor: C.bg,
|
|
332
|
+
borderTopWidth: 1,
|
|
333
|
+
borderColor: C.border,
|
|
334
|
+
padding: 18
|
|
335
|
+
},
|
|
336
|
+
children: [
|
|
337
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
338
|
+
import_react_native.View,
|
|
339
|
+
{
|
|
340
|
+
style: {
|
|
341
|
+
flexDirection: "row",
|
|
342
|
+
justifyContent: "space-between",
|
|
343
|
+
marginBottom: 12
|
|
344
|
+
},
|
|
345
|
+
children: [
|
|
346
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: C.emeraldSoft, fontWeight: "700", fontSize: 16 }, children: "Translation feedback" }),
|
|
347
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.TouchableOpacity, { onPress: onClose, accessibilityLabel: "Close", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: C.dim, fontSize: 20 }, children: "\xD7" }) })
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
),
|
|
351
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: "#f87171", marginBottom: 8 }, children: error }) : null,
|
|
352
|
+
!consented ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { children: [
|
|
353
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: C.text, lineHeight: 21 }, children: "Help improve the translations in this app. Your ratings and suggestions are sent to the app owner via Verbumia. Don\u2019t submit personal or sensitive information." }),
|
|
354
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
355
|
+
import_react_native.TouchableOpacity,
|
|
356
|
+
{
|
|
357
|
+
disabled: busy,
|
|
358
|
+
onPress: accept,
|
|
359
|
+
style: {
|
|
360
|
+
marginTop: 14,
|
|
361
|
+
backgroundColor: C.emerald,
|
|
362
|
+
borderRadius: 8,
|
|
363
|
+
padding: 14
|
|
364
|
+
},
|
|
365
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: "#03110c", fontWeight: "700", textAlign: "center" }, children: busy ? "\u2026" : `I accept the terms (v${client.tosVersion})` })
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.ScrollView, { 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." }) : strings.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
369
|
+
StringRow,
|
|
370
|
+
{
|
|
371
|
+
s,
|
|
372
|
+
client
|
|
373
|
+
},
|
|
374
|
+
`${s.namespace}:${s.key}`
|
|
375
|
+
)) })
|
|
376
|
+
]
|
|
377
|
+
}
|
|
378
|
+
) })
|
|
379
|
+
}
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
function StringRow(props) {
|
|
383
|
+
const { s, client } = props;
|
|
384
|
+
const [mine, setMine] = (0, import_react.useState)(s.my_rating);
|
|
385
|
+
const [show, setShow] = (0, import_react.useState)(false);
|
|
386
|
+
const [text, setText] = (0, import_react.useState)("");
|
|
387
|
+
const [sent, setSent] = (0, import_react.useState)(false);
|
|
388
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
389
|
+
import_react_native.View,
|
|
390
|
+
{
|
|
391
|
+
style: {
|
|
392
|
+
backgroundColor: C.panel,
|
|
393
|
+
borderWidth: 1,
|
|
394
|
+
borderColor: C.border,
|
|
395
|
+
borderRadius: 10,
|
|
396
|
+
padding: 12,
|
|
397
|
+
marginBottom: 10
|
|
398
|
+
},
|
|
399
|
+
children: [
|
|
400
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.Text, { style: { color: C.dim, fontSize: 12 }, children: [
|
|
401
|
+
s.namespace,
|
|
402
|
+
" \xB7 ",
|
|
403
|
+
s.key
|
|
404
|
+
] }),
|
|
405
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: C.text, fontSize: 15, marginVertical: 6 }, children: s.value }),
|
|
406
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: { flexDirection: "row" }, children: [
|
|
407
|
+
[1, 2, 3, 4, 5].map((n) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
408
|
+
import_react_native.TouchableOpacity,
|
|
409
|
+
{
|
|
410
|
+
accessibilityLabel: `${n} stars`,
|
|
411
|
+
onPress: () => {
|
|
412
|
+
setMine(n);
|
|
413
|
+
client.rate({
|
|
414
|
+
namespace: s.namespace,
|
|
415
|
+
key: s.key,
|
|
416
|
+
language: client.language,
|
|
417
|
+
translation_hash: s.translation_hash,
|
|
418
|
+
stars: n
|
|
419
|
+
});
|
|
420
|
+
},
|
|
421
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
422
|
+
import_react_native.Text,
|
|
423
|
+
{
|
|
424
|
+
style: {
|
|
425
|
+
fontSize: 22,
|
|
426
|
+
marginRight: 4,
|
|
427
|
+
color: mine && n <= mine ? C.emeraldSoft : C.border
|
|
428
|
+
},
|
|
429
|
+
children: "\u2605"
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
},
|
|
433
|
+
n
|
|
434
|
+
)),
|
|
435
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
436
|
+
import_react_native.TouchableOpacity,
|
|
437
|
+
{
|
|
438
|
+
onPress: () => setShow(!show),
|
|
439
|
+
style: { marginLeft: "auto" },
|
|
440
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: C.emeraldSoft }, children: sent ? "Suggested \u2713" : "Suggest" })
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
] }),
|
|
444
|
+
show ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: { marginTop: 10 }, children: [
|
|
445
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
446
|
+
import_react_native.TextInput,
|
|
447
|
+
{
|
|
448
|
+
value: text,
|
|
449
|
+
onChangeText: setText,
|
|
450
|
+
multiline: true,
|
|
451
|
+
numberOfLines: 3,
|
|
452
|
+
placeholder: "Your suggested translation\u2026",
|
|
453
|
+
placeholderTextColor: C.dim,
|
|
454
|
+
style: {
|
|
455
|
+
backgroundColor: C.bg,
|
|
456
|
+
color: C.text,
|
|
457
|
+
borderWidth: 1,
|
|
458
|
+
borderColor: C.border,
|
|
459
|
+
borderRadius: 6,
|
|
460
|
+
padding: 8
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
),
|
|
464
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
465
|
+
import_react_native.TouchableOpacity,
|
|
466
|
+
{
|
|
467
|
+
onPress: () => {
|
|
468
|
+
if (!text.trim()) return;
|
|
469
|
+
client.suggest({
|
|
470
|
+
namespace: s.namespace,
|
|
471
|
+
key: s.key,
|
|
472
|
+
language: client.language,
|
|
473
|
+
translation_hash: s.translation_hash,
|
|
474
|
+
suggested_text: text.trim()
|
|
475
|
+
});
|
|
476
|
+
setSent(true);
|
|
477
|
+
setShow(false);
|
|
478
|
+
setText("");
|
|
479
|
+
},
|
|
480
|
+
style: {
|
|
481
|
+
marginTop: 6,
|
|
482
|
+
backgroundColor: C.emerald,
|
|
483
|
+
borderRadius: 6,
|
|
484
|
+
padding: 10
|
|
485
|
+
},
|
|
486
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: { color: "#03110c", fontWeight: "700", textAlign: "center" }, children: "Send suggestion" })
|
|
487
|
+
}
|
|
488
|
+
)
|
|
489
|
+
] }) : null
|
|
490
|
+
]
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/native/plugin.tsx
|
|
496
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
497
|
+
function makeStore() {
|
|
498
|
+
let open = false;
|
|
499
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
500
|
+
return {
|
|
501
|
+
isOpen: () => open,
|
|
502
|
+
set(v) {
|
|
503
|
+
if (open !== v) {
|
|
504
|
+
open = v;
|
|
505
|
+
listeners.forEach((l) => l());
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
subscribe(l) {
|
|
509
|
+
listeners.add(l);
|
|
510
|
+
return () => listeners.delete(l);
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function feedbackPlugin(options) {
|
|
515
|
+
const store = makeStore();
|
|
516
|
+
let client = null;
|
|
517
|
+
function Outlet() {
|
|
518
|
+
const isOpen = (0, import_react2.useSyncExternalStore)(
|
|
519
|
+
store.subscribe,
|
|
520
|
+
store.isOpen,
|
|
521
|
+
store.isOpen
|
|
522
|
+
);
|
|
523
|
+
if (!client) return null;
|
|
524
|
+
const c = client;
|
|
525
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
526
|
+
FeedbackModal,
|
|
527
|
+
{
|
|
528
|
+
client: c,
|
|
529
|
+
visible: isOpen,
|
|
530
|
+
keys: options.keys,
|
|
531
|
+
onClose: () => {
|
|
532
|
+
store.set(false);
|
|
533
|
+
void c.flush();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
name: "@verbumia/feedback",
|
|
540
|
+
setup(ctx) {
|
|
541
|
+
client = new FeedbackClient({
|
|
542
|
+
apiBase: options.apiBase ?? ctx.config.apiBase ?? "https://api.verbumia.ca",
|
|
543
|
+
projectId: options.projectId ?? ctx.config.projectUuid,
|
|
544
|
+
language: options.language ?? ctx.config.defaultLocale,
|
|
545
|
+
endUserId: options.endUserId,
|
|
546
|
+
fetchImpl: options.fetchImpl
|
|
547
|
+
});
|
|
548
|
+
const controller = {
|
|
549
|
+
open: () => store.set(true),
|
|
550
|
+
close: () => {
|
|
551
|
+
store.set(false);
|
|
552
|
+
void client?.flush();
|
|
553
|
+
},
|
|
554
|
+
client
|
|
555
|
+
};
|
|
556
|
+
options.onReady?.(controller);
|
|
557
|
+
if (options.controllerRef) options.controllerRef.current = controller;
|
|
558
|
+
return () => {
|
|
559
|
+
if (options.controllerRef) options.controllerRef.current = null;
|
|
560
|
+
void client?.flush();
|
|
561
|
+
};
|
|
562
|
+
},
|
|
563
|
+
render: () => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Outlet, {})
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
567
|
+
0 && (module.exports = {
|
|
568
|
+
FeedbackClient,
|
|
569
|
+
FeedbackError,
|
|
570
|
+
FeedbackModal,
|
|
571
|
+
feedbackPlugin,
|
|
572
|
+
hasKeyRegistry,
|
|
573
|
+
resolveKeys
|
|
574
|
+
});
|
|
575
|
+
//# sourceMappingURL=index.cjs.map
|