@shellapps/experience-react 1.1.0 → 1.3.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/dist/index.mjs CHANGED
@@ -103,10 +103,527 @@ var ErrorBoundary = class extends React2.Component {
103
103
  };
104
104
  ErrorBoundary.contextType = ExperienceContext;
105
105
 
106
+ // src/FeedbackButton.tsx
107
+ import { useState, useEffect as useEffect2, useCallback, useRef as useRef2, useContext } from "react";
108
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
109
+ var TYPE_META = {
110
+ "bug": { icon: "\u{1F41B}", label: "Bug Report", color: "#ef4444" },
111
+ "feature-request": { icon: "\u{1F4A1}", label: "Feature Request", color: "#8b5cf6" },
112
+ "comment": { icon: "\u{1F4AC}", label: "Comment", color: "#3b82f6" },
113
+ "praise": { icon: "\u{1F389}", label: "Praise", color: "#10b981" }
114
+ };
115
+ var STATUS_LABELS = {
116
+ "new": "\u{1F195} New",
117
+ "acknowledged": "\u{1F440} Acknowledged",
118
+ "in-progress": "\u{1F527} In Progress",
119
+ "resolved": "\u2705 Resolved",
120
+ "wont-fix": "\u{1F6AB} Won't Fix"
121
+ };
122
+ function FeedbackButton({
123
+ position = "bottom-right",
124
+ label = "Feedback",
125
+ color = "#6366f1",
126
+ hidden = false,
127
+ apiUrl,
128
+ authToken
129
+ }) {
130
+ const experience = useContext(ExperienceContext);
131
+ const [isOpen, setIsOpen] = useState(false);
132
+ const [view, setView] = useState("list");
133
+ const [selectedType, setSelectedType] = useState("comment");
134
+ const [title, setTitle] = useState("");
135
+ const [content, setContent] = useState("");
136
+ const [submitting, setSubmitting] = useState(false);
137
+ const [items, setItems] = useState([]);
138
+ const [selectedItem, setSelectedItem] = useState(null);
139
+ const [replies, setReplies] = useState([]);
140
+ const [replyText, setReplyText] = useState("");
141
+ const [loading, setLoading] = useState(false);
142
+ const [screenshot, setScreenshot] = useState(null);
143
+ const panelRef = useRef2(null);
144
+ const baseUrl = apiUrl || experience?.config?.apiUrl || "https://experience.shellapps.com";
145
+ const appId = experience?.config?.appId || "";
146
+ const headers = useCallback(() => {
147
+ const h = { "Content-Type": "application/json" };
148
+ if (authToken) h["Authorization"] = `Bearer ${authToken}`;
149
+ return h;
150
+ }, [authToken]);
151
+ useEffect2(() => {
152
+ if (experience) {
153
+ experience.openFeedback = () => setIsOpen(true);
154
+ }
155
+ return () => {
156
+ if (experience) delete experience.openFeedback;
157
+ };
158
+ }, [experience]);
159
+ useEffect2(() => {
160
+ if (isOpen && view === "list") {
161
+ loadFeedback();
162
+ }
163
+ }, [isOpen, view]);
164
+ const loadFeedback = async () => {
165
+ setLoading(true);
166
+ try {
167
+ const res = await fetch(
168
+ `${baseUrl}/api/v1/feedback?appId=${appId}&limit=20`,
169
+ { headers: headers() }
170
+ );
171
+ if (res.ok) {
172
+ const data = await res.json();
173
+ setItems(data.items || []);
174
+ }
175
+ } catch {
176
+ }
177
+ setLoading(false);
178
+ };
179
+ const loadDetail = async (item) => {
180
+ setSelectedItem(item);
181
+ setView("detail");
182
+ try {
183
+ const res = await fetch(
184
+ `${baseUrl}/api/v1/feedback/${item._id}`,
185
+ { headers: headers() }
186
+ );
187
+ if (res.ok) {
188
+ const data = await res.json();
189
+ setReplies(data.replies || []);
190
+ }
191
+ } catch {
192
+ }
193
+ };
194
+ const captureScreenshot = async () => {
195
+ try {
196
+ const html2canvas = window.html2canvas;
197
+ if (!html2canvas) return;
198
+ const canvas = await html2canvas(document.body, { scale: 0.5, logging: false });
199
+ setScreenshot(canvas.toDataURL("image/jpeg", 0.6));
200
+ } catch {
201
+ }
202
+ };
203
+ const submit = async () => {
204
+ if (!title.trim() || !content.trim()) return;
205
+ setSubmitting(true);
206
+ try {
207
+ const ctx = {
208
+ userAgent: navigator.userAgent,
209
+ url: window.location.href,
210
+ viewport: { width: window.innerWidth, height: window.innerHeight },
211
+ locale: navigator.language,
212
+ referrer: document.referrer || void 0
213
+ };
214
+ const res = await fetch(`${baseUrl}/api/v1/feedback`, {
215
+ method: "POST",
216
+ headers: headers(),
217
+ body: JSON.stringify({
218
+ appId,
219
+ type: selectedType,
220
+ title: title.trim(),
221
+ content: content.trim(),
222
+ context: ctx,
223
+ screenshot: screenshot || void 0
224
+ })
225
+ });
226
+ if (res.ok) {
227
+ setTitle("");
228
+ setContent("");
229
+ setScreenshot(null);
230
+ setView("list");
231
+ loadFeedback();
232
+ }
233
+ } catch {
234
+ }
235
+ setSubmitting(false);
236
+ };
237
+ const submitReply = async () => {
238
+ if (!replyText.trim() || !selectedItem) return;
239
+ try {
240
+ const res = await fetch(`${baseUrl}/api/v1/feedback/${selectedItem._id}/reply`, {
241
+ method: "POST",
242
+ headers: headers(),
243
+ body: JSON.stringify({ content: replyText.trim() })
244
+ });
245
+ if (res.ok) {
246
+ const reply = await res.json();
247
+ setReplies((prev) => [...prev, reply]);
248
+ setReplyText("");
249
+ }
250
+ } catch {
251
+ }
252
+ };
253
+ const vote = async (id) => {
254
+ try {
255
+ await fetch(`${baseUrl}/api/v1/feedback/${id}/vote`, {
256
+ method: "POST",
257
+ headers: headers(),
258
+ body: JSON.stringify({ type: "upvote" })
259
+ });
260
+ loadFeedback();
261
+ } catch {
262
+ }
263
+ };
264
+ const posStyles = {
265
+ "bottom-right": { bottom: 20, right: 20 },
266
+ "bottom-left": { bottom: 20, left: 20 },
267
+ "top-right": { top: 20, right: 20 },
268
+ "top-left": { top: 20, left: 20 }
269
+ };
270
+ const panelPos = {
271
+ "bottom-right": { bottom: 70, right: 20 },
272
+ "bottom-left": { bottom: 70, left: 20 },
273
+ "top-right": { top: 70, right: 20 },
274
+ "top-left": { top: 70, left: 20 }
275
+ };
276
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
277
+ !hidden && /* @__PURE__ */ jsxs2(
278
+ "button",
279
+ {
280
+ onClick: () => setIsOpen(!isOpen),
281
+ style: {
282
+ position: "fixed",
283
+ ...posStyles[position],
284
+ zIndex: 99999,
285
+ background: color,
286
+ color: "#fff",
287
+ border: "none",
288
+ borderRadius: 28,
289
+ padding: "10px 20px",
290
+ fontSize: 14,
291
+ fontWeight: 600,
292
+ cursor: "pointer",
293
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
294
+ display: "flex",
295
+ alignItems: "center",
296
+ gap: 6
297
+ },
298
+ children: [
299
+ "\u{1F4AC} ",
300
+ label
301
+ ]
302
+ }
303
+ ),
304
+ isOpen && /* @__PURE__ */ jsxs2(
305
+ "div",
306
+ {
307
+ ref: panelRef,
308
+ style: {
309
+ position: "fixed",
310
+ ...panelPos[position],
311
+ zIndex: 1e5,
312
+ width: 380,
313
+ maxHeight: 520,
314
+ background: "#fff",
315
+ borderRadius: 12,
316
+ boxShadow: "0 8px 32px rgba(0,0,0,0.18)",
317
+ display: "flex",
318
+ flexDirection: "column",
319
+ overflow: "hidden",
320
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
321
+ fontSize: 14,
322
+ color: "#1f2937"
323
+ },
324
+ children: [
325
+ /* @__PURE__ */ jsxs2(
326
+ "div",
327
+ {
328
+ style: {
329
+ padding: "14px 16px",
330
+ background: color,
331
+ color: "#fff",
332
+ display: "flex",
333
+ justifyContent: "space-between",
334
+ alignItems: "center"
335
+ },
336
+ children: [
337
+ /* @__PURE__ */ jsx3("span", { style: { fontWeight: 600 }, children: view === "new" ? "New Feedback" : view === "detail" ? "Feedback" : "Feedback" }),
338
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8 }, children: [
339
+ view !== "list" && /* @__PURE__ */ jsx3(
340
+ "button",
341
+ {
342
+ onClick: () => {
343
+ setView("list");
344
+ setSelectedItem(null);
345
+ },
346
+ style: { background: "none", border: "none", color: "#fff", cursor: "pointer", fontSize: 14 },
347
+ children: "\u2190 Back"
348
+ }
349
+ ),
350
+ /* @__PURE__ */ jsx3(
351
+ "button",
352
+ {
353
+ onClick: () => setIsOpen(false),
354
+ style: { background: "none", border: "none", color: "#fff", cursor: "pointer", fontSize: 18 },
355
+ children: "\u2715"
356
+ }
357
+ )
358
+ ] })
359
+ ]
360
+ }
361
+ ),
362
+ /* @__PURE__ */ jsxs2("div", { style: { flex: 1, overflow: "auto", padding: 16 }, children: [
363
+ view === "list" && /* @__PURE__ */ jsxs2(Fragment, { children: [
364
+ /* @__PURE__ */ jsx3(
365
+ "button",
366
+ {
367
+ onClick: () => setView("new"),
368
+ style: {
369
+ width: "100%",
370
+ padding: "10px",
371
+ background: color,
372
+ color: "#fff",
373
+ border: "none",
374
+ borderRadius: 8,
375
+ cursor: "pointer",
376
+ fontWeight: 600,
377
+ marginBottom: 12
378
+ },
379
+ children: "+ New Feedback"
380
+ }
381
+ ),
382
+ loading ? /* @__PURE__ */ jsx3("p", { style: { textAlign: "center", color: "#9ca3af" }, children: "Loading\u2026" }) : items.length === 0 ? /* @__PURE__ */ jsx3("p", { style: { textAlign: "center", color: "#9ca3af" }, children: "No feedback yet" }) : items.map((item) => /* @__PURE__ */ jsxs2(
383
+ "div",
384
+ {
385
+ onClick: () => loadDetail(item),
386
+ style: {
387
+ padding: 10,
388
+ borderRadius: 8,
389
+ border: "1px solid #e5e7eb",
390
+ marginBottom: 8,
391
+ cursor: "pointer"
392
+ },
393
+ children: [
394
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: 4 }, children: [
395
+ /* @__PURE__ */ jsxs2("span", { children: [
396
+ TYPE_META[item.type]?.icon,
397
+ " ",
398
+ /* @__PURE__ */ jsx3("strong", { children: item.title })
399
+ ] }),
400
+ item.type === "feature-request" && /* @__PURE__ */ jsxs2(
401
+ "button",
402
+ {
403
+ onClick: (e) => {
404
+ e.stopPropagation();
405
+ vote(item._id);
406
+ },
407
+ style: {
408
+ background: "none",
409
+ border: "1px solid #d1d5db",
410
+ borderRadius: 4,
411
+ padding: "2px 6px",
412
+ cursor: "pointer",
413
+ fontSize: 12
414
+ },
415
+ children: [
416
+ "\u25B2 ",
417
+ item.vote_count
418
+ ]
419
+ }
420
+ )
421
+ ] }),
422
+ /* @__PURE__ */ jsxs2("div", { style: { fontSize: 12, color: "#6b7280" }, children: [
423
+ STATUS_LABELS[item.status],
424
+ " \xB7 ",
425
+ item.reply_count,
426
+ " replies \xB7 ",
427
+ new Date(item.created_at).toLocaleDateString()
428
+ ] })
429
+ ]
430
+ },
431
+ item._id
432
+ ))
433
+ ] }),
434
+ view === "new" && /* @__PURE__ */ jsxs2(Fragment, { children: [
435
+ /* @__PURE__ */ jsx3("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, marginBottom: 12 }, children: Object.entries(TYPE_META).map(
436
+ ([type, meta]) => /* @__PURE__ */ jsxs2(
437
+ "button",
438
+ {
439
+ onClick: () => setSelectedType(type),
440
+ style: {
441
+ padding: 8,
442
+ borderRadius: 8,
443
+ border: `2px solid ${selectedType === type ? meta.color : "#e5e7eb"}`,
444
+ background: selectedType === type ? `${meta.color}10` : "#fff",
445
+ cursor: "pointer",
446
+ fontSize: 13,
447
+ textAlign: "center"
448
+ },
449
+ children: [
450
+ meta.icon,
451
+ " ",
452
+ meta.label
453
+ ]
454
+ },
455
+ type
456
+ )
457
+ ) }),
458
+ /* @__PURE__ */ jsx3(
459
+ "input",
460
+ {
461
+ placeholder: "Title",
462
+ value: title,
463
+ onChange: (e) => setTitle(e.target.value),
464
+ style: {
465
+ width: "100%",
466
+ padding: 8,
467
+ borderRadius: 6,
468
+ border: "1px solid #d1d5db",
469
+ marginBottom: 8,
470
+ fontSize: 14,
471
+ boxSizing: "border-box"
472
+ }
473
+ }
474
+ ),
475
+ /* @__PURE__ */ jsx3(
476
+ "textarea",
477
+ {
478
+ placeholder: "Describe your feedback\u2026",
479
+ value: content,
480
+ onChange: (e) => setContent(e.target.value),
481
+ rows: 4,
482
+ style: {
483
+ width: "100%",
484
+ padding: 8,
485
+ borderRadius: 6,
486
+ border: "1px solid #d1d5db",
487
+ marginBottom: 8,
488
+ fontSize: 14,
489
+ resize: "vertical",
490
+ boxSizing: "border-box"
491
+ }
492
+ }
493
+ ),
494
+ window.html2canvas && /* @__PURE__ */ jsxs2(
495
+ "button",
496
+ {
497
+ onClick: captureScreenshot,
498
+ style: {
499
+ background: "none",
500
+ border: "1px solid #d1d5db",
501
+ borderRadius: 6,
502
+ padding: "6px 10px",
503
+ cursor: "pointer",
504
+ fontSize: 12,
505
+ marginBottom: 8,
506
+ color: screenshot ? "#10b981" : "#6b7280"
507
+ },
508
+ children: [
509
+ "\u{1F4F7} ",
510
+ screenshot ? "Screenshot captured \u2713" : "Capture screenshot"
511
+ ]
512
+ }
513
+ ),
514
+ /* @__PURE__ */ jsx3(
515
+ "button",
516
+ {
517
+ onClick: submit,
518
+ disabled: submitting || !title.trim() || !content.trim(),
519
+ style: {
520
+ width: "100%",
521
+ padding: 10,
522
+ background: submitting ? "#9ca3af" : color,
523
+ color: "#fff",
524
+ border: "none",
525
+ borderRadius: 8,
526
+ cursor: submitting ? "default" : "pointer",
527
+ fontWeight: 600
528
+ },
529
+ children: submitting ? "Submitting\u2026" : "Submit"
530
+ }
531
+ )
532
+ ] }),
533
+ view === "detail" && selectedItem && /* @__PURE__ */ jsxs2(Fragment, { children: [
534
+ /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 12 }, children: [
535
+ /* @__PURE__ */ jsxs2("div", { style: { fontSize: 12, color: TYPE_META[selectedItem.type]?.color, fontWeight: 600, marginBottom: 4 }, children: [
536
+ TYPE_META[selectedItem.type]?.icon,
537
+ " ",
538
+ TYPE_META[selectedItem.type]?.label
539
+ ] }),
540
+ /* @__PURE__ */ jsx3("h3", { style: { margin: "0 0 4px", fontSize: 16 }, children: selectedItem.title }),
541
+ /* @__PURE__ */ jsxs2("div", { style: { fontSize: 12, color: "#6b7280", marginBottom: 8 }, children: [
542
+ STATUS_LABELS[selectedItem.status],
543
+ " \xB7 ",
544
+ new Date(selectedItem.created_at).toLocaleDateString()
545
+ ] }),
546
+ /* @__PURE__ */ jsx3("p", { style: { margin: 0, lineHeight: 1.5 }, children: selectedItem.content })
547
+ ] }),
548
+ /* @__PURE__ */ jsx3("hr", { style: { border: "none", borderTop: "1px solid #e5e7eb", margin: "12px 0" } }),
549
+ /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 12 }, children: [
550
+ /* @__PURE__ */ jsxs2("strong", { style: { fontSize: 13 }, children: [
551
+ "Replies (",
552
+ replies.length,
553
+ ")"
554
+ ] }),
555
+ replies.map((r) => /* @__PURE__ */ jsxs2(
556
+ "div",
557
+ {
558
+ style: {
559
+ padding: 8,
560
+ marginTop: 8,
561
+ borderRadius: 6,
562
+ background: r.author_type === "developer" ? "#eff6ff" : "#f9fafb",
563
+ borderLeft: `3px solid ${r.author_type === "developer" ? "#3b82f6" : "#d1d5db"}`
564
+ },
565
+ children: [
566
+ /* @__PURE__ */ jsxs2("div", { style: { fontSize: 11, color: "#6b7280", marginBottom: 2 }, children: [
567
+ r.author_type === "developer" ? "\u{1F6E0}\uFE0F Developer" : "\u{1F464} You",
568
+ r.author_name ? ` \xB7 ${r.author_name}` : "",
569
+ " \xB7 ",
570
+ new Date(r.created_at).toLocaleDateString()
571
+ ] }),
572
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: 13 }, children: r.content })
573
+ ]
574
+ },
575
+ r._id
576
+ ))
577
+ ] }),
578
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 6 }, children: [
579
+ /* @__PURE__ */ jsx3(
580
+ "input",
581
+ {
582
+ placeholder: "Write a reply\u2026",
583
+ value: replyText,
584
+ onChange: (e) => setReplyText(e.target.value),
585
+ onKeyDown: (e) => {
586
+ if (e.key === "Enter") submitReply();
587
+ },
588
+ style: {
589
+ flex: 1,
590
+ padding: 8,
591
+ borderRadius: 6,
592
+ border: "1px solid #d1d5db",
593
+ fontSize: 13
594
+ }
595
+ }
596
+ ),
597
+ /* @__PURE__ */ jsx3(
598
+ "button",
599
+ {
600
+ onClick: submitReply,
601
+ disabled: !replyText.trim(),
602
+ style: {
603
+ padding: "8px 12px",
604
+ background: color,
605
+ color: "#fff",
606
+ border: "none",
607
+ borderRadius: 6,
608
+ cursor: "pointer",
609
+ fontSize: 13
610
+ },
611
+ children: "Send"
612
+ }
613
+ )
614
+ ] })
615
+ ] })
616
+ ] })
617
+ ]
618
+ }
619
+ )
620
+ ] });
621
+ }
622
+
106
623
  // src/hooks.ts
107
- import { useContext, useState, useCallback, useMemo } from "react";
624
+ import { useContext as useContext2, useState as useState2, useCallback as useCallback2, useMemo, useEffect as useEffect3 } from "react";
108
625
  function useExperience() {
109
- const ctx = useContext(ExperienceContext);
626
+ const ctx = useContext2(ExperienceContext);
110
627
  if (!ctx) throw new Error("useExperience must be used within ExperienceProvider");
111
628
  return ctx;
112
629
  }
@@ -121,28 +638,107 @@ function useFlag(flagName, defaultValue) {
121
638
  const experience = useExperience();
122
639
  return experience.getFlag(flagName, defaultValue);
123
640
  }
641
+ var translationCache = {};
642
+ var fetchPromises = /* @__PURE__ */ new Map();
124
643
  function useTranslation() {
125
- const [locale, setLocaleState] = useState(
126
- typeof navigator !== "undefined" ? navigator.language?.split("-")[0] || "en" : "en"
127
- );
128
- const t = useCallback((key, params) => {
129
- let result = key;
644
+ const experience = useExperience();
645
+ const config = experience.config || {};
646
+ const appId = config.appId || "";
647
+ const apiBaseUrl = config.apiUrl || config.baseUrl || "https://experience-api.shellapps.com";
648
+ const [locale, setLocaleState] = useState2(() => {
649
+ if (typeof window === "undefined") return "en";
650
+ const saved = localStorage.getItem(`exp_locale_${appId}`);
651
+ if (saved) return saved;
652
+ return navigator.language?.split("-")[0] || "en";
653
+ });
654
+ const [translations, setTranslations] = useState2(() => {
655
+ if (translationCache[locale]) return translationCache[locale];
656
+ if (typeof window !== "undefined") {
657
+ try {
658
+ const cached = localStorage.getItem(`exp_translations_${appId}_${locale}`);
659
+ if (cached) {
660
+ const parsed = JSON.parse(cached);
661
+ translationCache[locale] = parsed;
662
+ return parsed;
663
+ }
664
+ } catch {
665
+ }
666
+ }
667
+ return {};
668
+ });
669
+ const [locales, setLocales] = useState2([]);
670
+ const [isLoading, setIsLoading] = useState2(false);
671
+ const fetchTranslations = useCallback2(async (loc) => {
672
+ if (!appId) return {};
673
+ const cacheKey = `${appId}_${loc}`;
674
+ const existing = fetchPromises.get(cacheKey);
675
+ if (existing) return existing;
676
+ const promise = (async () => {
677
+ try {
678
+ const res = await fetch(`${apiBaseUrl}/api/v1/translations/${appId}/${loc}`);
679
+ if (!res.ok) return {};
680
+ const data = await res.json();
681
+ translationCache[loc] = data;
682
+ if (typeof window !== "undefined") {
683
+ try {
684
+ localStorage.setItem(`exp_translations_${appId}_${loc}`, JSON.stringify(data));
685
+ } catch {
686
+ }
687
+ }
688
+ return data;
689
+ } catch {
690
+ return {};
691
+ } finally {
692
+ fetchPromises.delete(cacheKey);
693
+ }
694
+ })();
695
+ fetchPromises.set(cacheKey, promise);
696
+ return promise;
697
+ }, [appId, apiBaseUrl]);
698
+ useEffect3(() => {
699
+ if (!appId) return;
700
+ fetch(`${apiBaseUrl}/api/v1/translations/${appId}/locales`).then((res) => res.ok ? res.json() : { locales: [] }).then((data) => setLocales(data.locales || [])).catch(() => {
701
+ });
702
+ }, [appId, apiBaseUrl]);
703
+ useEffect3(() => {
704
+ if (!appId) return;
705
+ if (translationCache[locale]) {
706
+ setTranslations(translationCache[locale]);
707
+ return;
708
+ }
709
+ setIsLoading(true);
710
+ fetchTranslations(locale).then((data) => {
711
+ setTranslations(data);
712
+ setIsLoading(false);
713
+ });
714
+ }, [locale, appId, fetchTranslations]);
715
+ const t = useCallback2((key, params) => {
716
+ let result = translations[key] ?? key;
717
+ if (result.includes("||") && params && "count" in params) {
718
+ const parts = result.split("||");
719
+ const count = Number(params.count);
720
+ result = count === 1 ? parts[0] ?? result : parts[1] ?? result;
721
+ }
130
722
  if (params) {
131
723
  for (const [k, v] of Object.entries(params)) {
132
724
  result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
133
725
  }
134
726
  }
135
727
  return result;
136
- }, []);
137
- const setLocale = useCallback((newLocale) => {
728
+ }, [translations]);
729
+ const setLocale = useCallback2((newLocale) => {
138
730
  setLocaleState(newLocale);
139
- }, []);
140
- return { t, locale, setLocale, locales: [] };
731
+ if (typeof window !== "undefined") {
732
+ localStorage.setItem(`exp_locale_${appId}`, newLocale);
733
+ }
734
+ }, [appId]);
735
+ return { t, locale, setLocale, locales, isLoading };
141
736
  }
142
737
  export {
143
738
  ErrorBoundary,
144
739
  ExperienceContext,
145
740
  ExperienceProvider,
741
+ FeedbackButton,
146
742
  useExperience,
147
743
  useFlag,
148
744
  useTrack,