@mushi-mushi/react-native 0.13.1 → 0.15.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/README.md CHANGED
@@ -115,3 +115,9 @@ See the full [CHANGELOG](https://github.com/kensaurus/mushi-mushi/blob/master/pa
115
115
  ## License
116
116
 
117
117
  MIT
118
+
119
+
120
+ <!-- mushi-readme-stats-footer -->
121
+ ---
122
+
123
+ <sub>Monorepo scale (June 2026): 43 edge functions · 234 SQL migrations · 13 outbound plugins · 11 inbound adapters. Canonical counts: <a href="https://github.com/kensaurus/mushi-mushi/blob/master/docs/stats.md">docs/stats.md</a> · <code>pnpm docs-stats</code></sub>
package/dist/index.cjs CHANGED
@@ -319,7 +319,12 @@ var CATEGORIES = [
319
319
  { key: "confusing", emoji: "\u{1F615}", label: "Confusing" },
320
320
  { key: "other", emoji: "\u{1F4AC}", label: "Other" }
321
321
  ];
322
- var MushiBottomSheet = ({ visible, onClose }) => {
322
+ var MushiBottomSheet = ({
323
+ visible,
324
+ onClose,
325
+ screenshotDataUrl,
326
+ onClearScreenshot
327
+ }) => {
323
328
  const mushi = useMushiContext();
324
329
  const scheme = (0, import_react_native2.useColorScheme)();
325
330
  const dark = scheme === "dark";
@@ -328,6 +333,47 @@ var MushiBottomSheet = ({ visible, onClose }) => {
328
333
  const [category, setCategory] = (0, import_react.useState)(null);
329
334
  const [description, setDescription] = (0, import_react.useState)("");
330
335
  const [phase, setPhase] = (0, import_react.useState)("form");
336
+ const [sheetTab, setSheetTab] = (0, import_react.useState)("report");
337
+ const [inboxReports, setInboxReports] = (0, import_react.useState)([]);
338
+ const [inboxLoading, setInboxLoading] = (0, import_react.useState)(false);
339
+ const [selectedReportId, setSelectedReportId] = (0, import_react.useState)(null);
340
+ const [threadComments, setThreadComments] = (0, import_react.useState)([]);
341
+ const [replyText, setReplyText] = (0, import_react.useState)("");
342
+ const [screenshotAttached, setScreenshotAttached] = (0, import_react.useState)(true);
343
+ (0, import_react.useEffect)(() => {
344
+ if (visible) {
345
+ setScreenshotAttached(true);
346
+ if (sheetTab === "inbox") void loadInbox();
347
+ }
348
+ }, [visible, sheetTab]);
349
+ const loadInbox = (0, import_react.useCallback)(async () => {
350
+ if (!mushi?.listMyReports) return;
351
+ setInboxLoading(true);
352
+ try {
353
+ const rows = await mushi.listMyReports();
354
+ setInboxReports(rows);
355
+ } finally {
356
+ setInboxLoading(false);
357
+ }
358
+ }, [mushi]);
359
+ const openThread = (0, import_react.useCallback)(async (reportId) => {
360
+ setSelectedReportId(reportId);
361
+ if (!mushi?.listMyComments) return;
362
+ const comments = await mushi.listMyComments(reportId);
363
+ setThreadComments(comments);
364
+ }, [mushi]);
365
+ const sendReply = (0, import_react.useCallback)(async () => {
366
+ if (!mushi?.replyToReport || !selectedReportId || !replyText.trim()) return;
367
+ await mushi.replyToReport(selectedReportId, replyText.trim());
368
+ setReplyText("");
369
+ await openThread(selectedReportId);
370
+ }, [mushi, selectedReportId, replyText, openThread]);
371
+ const submitFeedback = (0, import_react.useCallback)(async (signal) => {
372
+ if (!mushi?.submitFeedbackSignal || !selectedReportId) return;
373
+ await mushi.submitFeedbackSignal(selectedReportId, signal);
374
+ await loadInbox();
375
+ await openThread(selectedReportId);
376
+ }, [mushi, selectedReportId, loadInbox, openThread]);
331
377
  const resetForm = (0, import_react.useCallback)(() => {
332
378
  setCategory(null);
333
379
  setDescription("");
@@ -399,13 +445,18 @@ var MushiBottomSheet = ({ visible, onClose }) => {
399
445
  if (!category || !description.trim() || !mushi) return;
400
446
  setPhase("sending");
401
447
  try {
402
- await mushi.submitReport({ category, description: description.trim() });
448
+ await mushi.submitReport({
449
+ category,
450
+ description: description.trim(),
451
+ screenshotDataUrl: screenshotDataUrl && screenshotAttached ? screenshotDataUrl : void 0
452
+ });
403
453
  setPhase("sent");
404
454
  setTimeout(handleClose, 1400);
405
455
  } catch {
406
456
  setPhase("form");
407
457
  }
408
458
  };
459
+ const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
409
460
  const colors = dark ? { bg: "#1c1c1e", text: "#f2f2f7", sub: "#8e8e93", card: "#2c2c2e", accent: "#0a84ff", border: "#38383a", backdrop: "rgba(0,0,0,0.6)" } : { bg: "#ffffff", text: "#1c1c1e", sub: "#8e8e93", card: "#f2f2f7", accent: "#007aff", border: "#e5e5ea", backdrop: "rgba(0,0,0,0.35)" };
410
461
  const canSubmit = !!category && description.trim().length > 0 && phase === "form";
411
462
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
@@ -430,7 +481,44 @@ var MushiBottomSheet = ({ visible, onClose }) => {
430
481
  ],
431
482
  children: [
432
483
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
433
- phase === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.sentWrap, children: [
484
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.tabRow, children: ["report", "inbox"].map((tab) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
485
+ import_react_native2.TouchableOpacity,
486
+ {
487
+ onPress: () => {
488
+ setSheetTab(tab);
489
+ if (tab === "inbox") void loadInbox();
490
+ else setSelectedReportId(null);
491
+ },
492
+ style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
493
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.accent : colors.sub }], children: tab === "report" ? "Report" : "Inbox" })
494
+ },
495
+ tab
496
+ )) }),
497
+ sheetTab === "inbox" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.body, children: inboxLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub }, children: "Loading\u2026" }) : selectedReportId ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
498
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { onPress: () => setSelectedReportId(null), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.accent, marginBottom: 8 }, children: "\u2190 Back" }) }),
499
+ threadComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: [s.threadBubble, { backgroundColor: colors.card }], children: [
500
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, fontSize: 11 }, children: c.author_kind }),
501
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.text }, children: c.body })
502
+ ] }, c.id)),
503
+ inboxReports.find((r) => r.id === selectedReportId)?.status === "fixed" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.verifyRow, children: [
504
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: s.submitText, children: "Yes, fixed" }) }),
505
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.border }], onPress: () => submitFeedback("not_fixed"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.text }], children: "Not fixed" }) })
506
+ ] }),
507
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
508
+ import_react_native2.TextInput,
509
+ {
510
+ style: [s.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border, minHeight: 48 }],
511
+ placeholder: "Reply\u2026",
512
+ placeholderTextColor: colors.sub,
513
+ value: replyText,
514
+ onChangeText: setReplyText
515
+ }
516
+ ),
517
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: s.submitText, children: "Send" }) })
518
+ ] }) : inboxReports.map((r) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
519
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.text, fontWeight: "600" }, numberOfLines: 1, children: (r.summary ?? r.description ?? "Report").slice(0, 60) }),
520
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, fontSize: 11 }, children: r.status })
521
+ ] }, r.id)) }) : phase === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.sentWrap, children: [
434
522
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentEmoji], children: "\u2705" }),
435
523
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentText, { color: colors.text }], children: "Report sent!" })
436
524
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.body, children: [
@@ -486,6 +574,32 @@ var MushiBottomSheet = ({ visible, onClose }) => {
486
574
  editable: phase === "form"
487
575
  }
488
576
  ),
577
+ activeScreenshot ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotRow, children: [
578
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
579
+ import_react_native2.Image,
580
+ {
581
+ source: { uri: activeScreenshot },
582
+ style: s.screenshotThumb,
583
+ accessibilityLabel: "Attached screenshot"
584
+ }
585
+ ),
586
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotMeta, children: [
587
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotLabel, { color: colors.text }], children: "Screenshot attached" }),
588
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotSub, { color: colors.sub }], children: "Helps the team see the issue" })
589
+ ] }),
590
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
591
+ import_react_native2.TouchableOpacity,
592
+ {
593
+ onPress: () => {
594
+ setScreenshotAttached(false);
595
+ onClearScreenshot?.();
596
+ },
597
+ hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
598
+ accessibilityLabel: "Remove screenshot",
599
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
600
+ }
601
+ )
602
+ ] }) : null,
489
603
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
490
604
  import_react_native2.TouchableOpacity,
491
605
  {
@@ -598,6 +712,76 @@ var s = import_react_native2.StyleSheet.create({
598
712
  sentText: {
599
713
  fontSize: 20,
600
714
  fontWeight: "600"
715
+ },
716
+ screenshotRow: {
717
+ flexDirection: "row",
718
+ alignItems: "center",
719
+ gap: 10,
720
+ marginBottom: 12,
721
+ paddingHorizontal: 10,
722
+ paddingVertical: 8,
723
+ borderRadius: 10,
724
+ backgroundColor: "rgba(128,128,128,0.1)"
725
+ },
726
+ screenshotThumb: {
727
+ width: 48,
728
+ height: 36,
729
+ borderRadius: 4,
730
+ backgroundColor: "#000",
731
+ flexShrink: 0
732
+ },
733
+ screenshotMeta: {
734
+ flex: 1,
735
+ gap: 2
736
+ },
737
+ screenshotLabel: {
738
+ fontSize: 13,
739
+ fontWeight: "600"
740
+ },
741
+ screenshotSub: {
742
+ fontSize: 11
743
+ },
744
+ screenshotRemove: {
745
+ fontSize: 16,
746
+ fontWeight: "700",
747
+ paddingHorizontal: 4
748
+ },
749
+ tabRow: {
750
+ flexDirection: "row",
751
+ borderBottomWidth: import_react_native2.StyleSheet.hairlineWidth,
752
+ borderBottomColor: "#ccc",
753
+ marginBottom: 8
754
+ },
755
+ tabBtn: {
756
+ flex: 1,
757
+ alignItems: "center",
758
+ paddingVertical: 10,
759
+ borderBottomWidth: 2,
760
+ borderBottomColor: "transparent"
761
+ },
762
+ tabLabel: {
763
+ fontSize: 14,
764
+ fontWeight: "600"
765
+ },
766
+ inboxRow: {
767
+ paddingVertical: 12,
768
+ borderBottomWidth: import_react_native2.StyleSheet.hairlineWidth
769
+ },
770
+ threadBubble: {
771
+ borderRadius: 10,
772
+ padding: 10,
773
+ marginBottom: 8
774
+ },
775
+ verifyRow: {
776
+ flexDirection: "row",
777
+ gap: 8,
778
+ marginBottom: 12
779
+ },
780
+ verifyBtn: {
781
+ flex: 1,
782
+ borderRadius: 10,
783
+ paddingVertical: 10,
784
+ alignItems: "center"
601
785
  }
602
786
  });
603
787
 
@@ -683,6 +867,15 @@ function MushiProvider({ children, ...config }) {
683
867
  const queueRef = (0, import_react3.useRef)(null);
684
868
  const apiClientRef = (0, import_react3.useRef)(null);
685
869
  const [sheetVisible, setSheetVisible] = (0, import_react3.useState)(false);
870
+ const [sheetScreenshot, setSheetScreenshot] = (0, import_react3.useState)(null);
871
+ const reporterTokenRef = (0, import_react3.useRef)(`rn-${config.projectId}-anon`);
872
+ let resolveReporterTokenReady = () => {
873
+ };
874
+ const reporterTokenReadyRef = (0, import_react3.useRef)(
875
+ new Promise((resolve) => {
876
+ resolveReporterTokenReady = resolve;
877
+ })
878
+ );
686
879
  if (config.endpoint !== void 0 && config.endpoint.trim() === "") {
687
880
  throw new Error(
688
881
  "[MushiProvider] endpoint is set to an empty string. Set endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api, or omit it to use the cloud default."
@@ -708,6 +901,27 @@ function MushiProvider({ children, ...config }) {
708
901
  });
709
902
  queueRef.current.flush().catch(() => {
710
903
  });
904
+ (async () => {
905
+ const TOKEN_KEY = "@mushi:reporter_token";
906
+ try {
907
+ const AsyncStorage = (await import("@react-native-async-storage/async-storage")).default;
908
+ const existing = await AsyncStorage.getItem(TOKEN_KEY);
909
+ if (existing && existing.startsWith("mushi_")) {
910
+ reporterTokenRef.current = existing;
911
+ } else {
912
+ const fresh = `mushi_${(0, import_core.newUuid)()}`;
913
+ reporterTokenRef.current = fresh;
914
+ await AsyncStorage.setItem(TOKEN_KEY, fresh);
915
+ }
916
+ resolveReporterTokenReady();
917
+ } catch {
918
+ if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
919
+ reporterTokenRef.current = `mushi_${(0, import_core.newUuid)()}`;
920
+ }
921
+ resolveReporterTokenReady();
922
+ }
923
+ })().catch(() => {
924
+ });
711
925
  return () => {
712
926
  consoleRef.current?.restore();
713
927
  networkRef.current?.restore();
@@ -727,7 +941,25 @@ function MushiProvider({ children, ...config }) {
727
941
  }
728
942
  return () => unsubscribe?.();
729
943
  }, []);
730
- const open = (0, import_react3.useCallback)(() => setSheetVisible(true), []);
944
+ const open = (0, import_react3.useCallback)(() => {
945
+ let vshot = null;
946
+ try {
947
+ vshot = require("react-native-view-shot");
948
+ } catch {
949
+ }
950
+ if (!vshot) {
951
+ setSheetScreenshot(null);
952
+ setSheetVisible(true);
953
+ return;
954
+ }
955
+ vshot.captureScreen({ format: "jpg", quality: 0.7, result: "data-uri" }).then((url) => {
956
+ setSheetScreenshot(url);
957
+ }).catch(() => {
958
+ setSheetScreenshot(null);
959
+ }).finally(() => {
960
+ setSheetVisible(true);
961
+ });
962
+ }, []);
731
963
  const close = (0, import_react3.useCallback)(() => setSheetVisible(false), []);
732
964
  const attachTo = (0, import_react3.useCallback)(() => ({ onPress: open }), [open]);
733
965
  (0, import_react3.useEffect)(() => {
@@ -761,8 +993,16 @@ function MushiProvider({ children, ...config }) {
761
993
  const submitReport = (0, import_react3.useCallback)(
762
994
  async (data) => {
763
995
  const deviceInfo = getDeviceInfo();
996
+ const contextMeta = {};
997
+ if (userRef.current?.id) contextMeta.userId = userRef.current.id;
998
+ if (userRef.current?.email) contextMeta.userEmail = userRef.current.email;
999
+ if (userRef.current?.name) contextMeta.userName = userRef.current.name;
1000
+ if (screenRef.current?.name) contextMeta.screenName = screenRef.current.name;
1001
+ if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
1002
+ if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
1003
+ const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
764
1004
  const report = {
765
- id: `rn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1005
+ id: (0, import_core.newUuid)(),
766
1006
  projectId: config.projectId,
767
1007
  category: data.category,
768
1008
  description: data.description,
@@ -771,7 +1011,7 @@ function MushiProvider({ children, ...config }) {
771
1011
  platform: deviceInfo.platform ?? "mobile",
772
1012
  language: deviceInfo.locale ?? "en",
773
1013
  viewport: { width: deviceInfo.screenWidth ?? 0, height: deviceInfo.screenHeight ?? 0 },
774
- url: "",
1014
+ url: screenRef.current?.route ?? "",
775
1015
  referrer: "",
776
1016
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
777
1017
  timezone: deviceInfo.timezone ?? "UTC"
@@ -781,7 +1021,9 @@ function MushiProvider({ children, ...config }) {
781
1021
  ...entry,
782
1022
  status: entry.status ?? 0
783
1023
  })) ?? [],
784
- reporterToken: `rn-${config.projectId}-anon`,
1024
+ screenshotDataUrl: data.screenshotDataUrl,
1025
+ metadata: merged,
1026
+ reporterToken: reporterTokenRef.current,
785
1027
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
786
1028
  };
787
1029
  const client = apiClientRef.current;
@@ -791,7 +1033,7 @@ function MushiProvider({ children, ...config }) {
791
1033
  await queueRef.current?.enqueue(report);
792
1034
  }
793
1035
  },
794
- [config.projectId, config.apiKey, apiEndpoint]
1036
+ [config.projectId, apiEndpoint]
795
1037
  );
796
1038
  const userRef = (0, import_react3.useRef)(null);
797
1039
  const metadataRef = (0, import_react3.useRef)({});
@@ -842,6 +1084,45 @@ function MushiProvider({ children, ...config }) {
842
1084
  setScreen(screen) {
843
1085
  screenRef.current = screen;
844
1086
  },
1087
+ // Reporter API — scoped to the device's persistent reporter token
1088
+ async listMyReports() {
1089
+ const client = apiClientRef.current;
1090
+ if (!client) return [];
1091
+ await reporterTokenReadyRef.current;
1092
+ const res = await client.listReporterReports(reporterTokenRef.current);
1093
+ return res.ok ? res.data?.reports ?? [] : [];
1094
+ },
1095
+ async listMyComments(reportId) {
1096
+ const client = apiClientRef.current;
1097
+ if (!client) return [];
1098
+ const res = await client.listReporterComments(reportId, reporterTokenRef.current);
1099
+ return res.ok ? res.data.comments ?? [] : [];
1100
+ },
1101
+ async replyToReport(reportId, body) {
1102
+ const client = apiClientRef.current;
1103
+ if (!client) return null;
1104
+ const res = await client.replyToReporterReport(reportId, reporterTokenRef.current, body);
1105
+ return res.ok ? res.data.comment : null;
1106
+ },
1107
+ async submitFeedbackSignal(reportId, signal, note) {
1108
+ const client = apiClientRef.current;
1109
+ if (!client) return null;
1110
+ const res = await client.replyToReporterReport(reportId, reporterTokenRef.current, note ?? "", signal);
1111
+ return res.ok ? res.data.feedback ?? null : null;
1112
+ },
1113
+ async reopenReport(reportId, note) {
1114
+ const client = apiClientRef.current;
1115
+ if (!client) return null;
1116
+ const res = await client.reopenReporterReport(reportId, reporterTokenRef.current, note);
1117
+ return res.ok ? res.data.outcome : null;
1118
+ },
1119
+ // Leaderboard — SDK-public anonymized hall-of-fame
1120
+ async getHallOfFame(limit = 10) {
1121
+ const client = apiClientRef.current;
1122
+ if (!client) return [];
1123
+ const res = await client.getHallOfFame(limit);
1124
+ return res.ok ? res.data.data ?? [] : [];
1125
+ },
845
1126
  // Rewards (P1)
846
1127
  recordActivity(action, metadata) {
847
1128
  if (!config.rewards?.enabled) return;
@@ -883,7 +1164,15 @@ function MushiProvider({ children, ...config }) {
883
1164
  inset: config.widget?.inset
884
1165
  }
885
1166
  ),
886
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MushiBottomSheet, { visible: sheetVisible, onClose: close })
1167
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1168
+ MushiBottomSheet,
1169
+ {
1170
+ visible: sheetVisible,
1171
+ onClose: close,
1172
+ screenshotDataUrl: sheetScreenshot ?? void 0,
1173
+ onClearScreenshot: () => setSheetScreenshot(null)
1174
+ }
1175
+ )
887
1176
  ] });
888
1177
  }
889
1178
  function useMushiContext() {
package/dist/index.d.cts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
3
  import { ReactNode, FC, ReactElement } from 'react';
4
- import { MushiRewardsConfig, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
4
+ import { MushiRewardsConfig, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
5
+ export { MushiHallOfFameEntry, MushiReporterComment, MushiReporterReport } from '@mushi-mushi/core';
5
6
 
6
7
  interface ConsoleEntry {
7
8
  level: 'log' | 'warn' | 'error';
@@ -69,15 +70,18 @@ interface MushiRNConfig {
69
70
  };
70
71
  rewards?: MushiRewardsConfig;
71
72
  }
73
+
72
74
  interface MushiRNInstance {
73
75
  open(): void;
74
76
  close(): void;
75
77
  attachTo(): {
76
78
  onPress: () => void;
77
79
  };
80
+ /** screenshotDataUrl is optional — pass `undefined` if not captured or removed by the user. */
78
81
  submitReport(data: {
79
82
  description: string;
80
83
  category: string;
84
+ screenshotDataUrl?: string;
81
85
  }): Promise<void>;
82
86
  getDeviceInfo(): ReturnType<typeof getDeviceInfo>;
83
87
  getConsoleEntries(): ReturnType<ReturnType<typeof setupConsoleCapture>['getEntries']>;
@@ -97,6 +101,18 @@ interface MushiRNInstance {
97
101
  route?: string;
98
102
  feature?: string;
99
103
  }): void;
104
+ /** List this device's reports ordered by most recent. Returns [] on failure. */
105
+ listMyReports(): Promise<MushiReporterReport[]>;
106
+ /** List admin + reporter comments on a specific report. */
107
+ listMyComments(reportId: string): Promise<MushiReporterComment[]>;
108
+ /** Post a reporter reply on a report thread. Returns the new comment or null on failure. */
109
+ replyToReport(reportId: string, body: string): Promise<MushiReporterComment | null>;
110
+ /** Record a reporter feedback signal (e.g. `confirms`, `not_fixed`) on a report. Returns the outcome or null. */
111
+ submitFeedbackSignal(reportId: string, signal: string, note?: string): Promise<Record<string, unknown> | null>;
112
+ /** Reopen a report the reporter flagged as not fixed. Returns the reopen outcome or null. */
113
+ reopenReport(reportId: string, note?: string): Promise<Record<string, unknown> | null>;
114
+ /** Fetch the project's top contributors by points (max 50). */
115
+ getHallOfFame(limit?: number): Promise<MushiHallOfFameEntry[]>;
100
116
  /** Manually record a host-defined activity event. */
101
117
  recordActivity(action: string, metadata?: Record<string, unknown>): void;
102
118
  /** Returns the current user's tier. */
@@ -153,6 +169,10 @@ declare function useMushiWidget(): {
153
169
  interface MushiBottomSheetProps {
154
170
  visible: boolean;
155
171
  onClose: () => void;
172
+ /** Base64 data-URI of the screenshot captured before the sheet opened. Optional. */
173
+ screenshotDataUrl?: string;
174
+ /** Called when the user removes the attached screenshot. */
175
+ onClearScreenshot?: () => void;
156
176
  }
157
177
  declare const MushiBottomSheet: FC<MushiBottomSheetProps>;
158
178
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
3
  import { ReactNode, FC, ReactElement } from 'react';
4
- import { MushiRewardsConfig, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
4
+ import { MushiRewardsConfig, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
5
+ export { MushiHallOfFameEntry, MushiReporterComment, MushiReporterReport } from '@mushi-mushi/core';
5
6
 
6
7
  interface ConsoleEntry {
7
8
  level: 'log' | 'warn' | 'error';
@@ -69,15 +70,18 @@ interface MushiRNConfig {
69
70
  };
70
71
  rewards?: MushiRewardsConfig;
71
72
  }
73
+
72
74
  interface MushiRNInstance {
73
75
  open(): void;
74
76
  close(): void;
75
77
  attachTo(): {
76
78
  onPress: () => void;
77
79
  };
80
+ /** screenshotDataUrl is optional — pass `undefined` if not captured or removed by the user. */
78
81
  submitReport(data: {
79
82
  description: string;
80
83
  category: string;
84
+ screenshotDataUrl?: string;
81
85
  }): Promise<void>;
82
86
  getDeviceInfo(): ReturnType<typeof getDeviceInfo>;
83
87
  getConsoleEntries(): ReturnType<ReturnType<typeof setupConsoleCapture>['getEntries']>;
@@ -97,6 +101,18 @@ interface MushiRNInstance {
97
101
  route?: string;
98
102
  feature?: string;
99
103
  }): void;
104
+ /** List this device's reports ordered by most recent. Returns [] on failure. */
105
+ listMyReports(): Promise<MushiReporterReport[]>;
106
+ /** List admin + reporter comments on a specific report. */
107
+ listMyComments(reportId: string): Promise<MushiReporterComment[]>;
108
+ /** Post a reporter reply on a report thread. Returns the new comment or null on failure. */
109
+ replyToReport(reportId: string, body: string): Promise<MushiReporterComment | null>;
110
+ /** Record a reporter feedback signal (e.g. `confirms`, `not_fixed`) on a report. Returns the outcome or null. */
111
+ submitFeedbackSignal(reportId: string, signal: string, note?: string): Promise<Record<string, unknown> | null>;
112
+ /** Reopen a report the reporter flagged as not fixed. Returns the reopen outcome or null. */
113
+ reopenReport(reportId: string, note?: string): Promise<Record<string, unknown> | null>;
114
+ /** Fetch the project's top contributors by points (max 50). */
115
+ getHallOfFame(limit?: number): Promise<MushiHallOfFameEntry[]>;
100
116
  /** Manually record a host-defined activity event. */
101
117
  recordActivity(action: string, metadata?: Record<string, unknown>): void;
102
118
  /** Returns the current user's tier. */
@@ -153,6 +169,10 @@ declare function useMushiWidget(): {
153
169
  interface MushiBottomSheetProps {
154
170
  visible: boolean;
155
171
  onClose: () => void;
172
+ /** Base64 data-URI of the screenshot captured before the sheet opened. Optional. */
173
+ screenshotDataUrl?: string;
174
+ /** Called when the user removes the attached screenshot. */
175
+ onClearScreenshot?: () => void;
156
176
  }
157
177
  declare const MushiBottomSheet: FC<MushiBottomSheetProps>;
158
178
 
package/dist/index.js CHANGED
@@ -17,7 +17,8 @@ import {
17
17
  } from "react";
18
18
  import {
19
19
  createApiClient,
20
- DEFAULT_API_ENDPOINT
20
+ DEFAULT_API_ENDPOINT,
21
+ newUuid
21
22
  } from "@mushi-mushi/core";
22
23
 
23
24
  // src/capture/console-capture.ts
@@ -288,6 +289,7 @@ import {
288
289
  View,
289
290
  Text,
290
291
  TextInput,
292
+ Image,
291
293
  TouchableOpacity,
292
294
  Animated,
293
295
  PanResponder,
@@ -297,7 +299,7 @@ import {
297
299
  KeyboardAvoidingView,
298
300
  useColorScheme
299
301
  } from "react-native";
300
- import { jsx, jsxs } from "react/jsx-runtime";
302
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
301
303
  var { height: SCREEN_HEIGHT } = Dimensions2.get("window");
302
304
  var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
303
305
  var DISMISS_THRESHOLD = 80;
@@ -308,7 +310,12 @@ var CATEGORIES = [
308
310
  { key: "confusing", emoji: "\u{1F615}", label: "Confusing" },
309
311
  { key: "other", emoji: "\u{1F4AC}", label: "Other" }
310
312
  ];
311
- var MushiBottomSheet = ({ visible, onClose }) => {
313
+ var MushiBottomSheet = ({
314
+ visible,
315
+ onClose,
316
+ screenshotDataUrl,
317
+ onClearScreenshot
318
+ }) => {
312
319
  const mushi = useMushiContext();
313
320
  const scheme = useColorScheme();
314
321
  const dark = scheme === "dark";
@@ -317,6 +324,47 @@ var MushiBottomSheet = ({ visible, onClose }) => {
317
324
  const [category, setCategory] = useState(null);
318
325
  const [description, setDescription] = useState("");
319
326
  const [phase, setPhase] = useState("form");
327
+ const [sheetTab, setSheetTab] = useState("report");
328
+ const [inboxReports, setInboxReports] = useState([]);
329
+ const [inboxLoading, setInboxLoading] = useState(false);
330
+ const [selectedReportId, setSelectedReportId] = useState(null);
331
+ const [threadComments, setThreadComments] = useState([]);
332
+ const [replyText, setReplyText] = useState("");
333
+ const [screenshotAttached, setScreenshotAttached] = useState(true);
334
+ useEffect(() => {
335
+ if (visible) {
336
+ setScreenshotAttached(true);
337
+ if (sheetTab === "inbox") void loadInbox();
338
+ }
339
+ }, [visible, sheetTab]);
340
+ const loadInbox = useCallback(async () => {
341
+ if (!mushi?.listMyReports) return;
342
+ setInboxLoading(true);
343
+ try {
344
+ const rows = await mushi.listMyReports();
345
+ setInboxReports(rows);
346
+ } finally {
347
+ setInboxLoading(false);
348
+ }
349
+ }, [mushi]);
350
+ const openThread = useCallback(async (reportId) => {
351
+ setSelectedReportId(reportId);
352
+ if (!mushi?.listMyComments) return;
353
+ const comments = await mushi.listMyComments(reportId);
354
+ setThreadComments(comments);
355
+ }, [mushi]);
356
+ const sendReply = useCallback(async () => {
357
+ if (!mushi?.replyToReport || !selectedReportId || !replyText.trim()) return;
358
+ await mushi.replyToReport(selectedReportId, replyText.trim());
359
+ setReplyText("");
360
+ await openThread(selectedReportId);
361
+ }, [mushi, selectedReportId, replyText, openThread]);
362
+ const submitFeedback = useCallback(async (signal) => {
363
+ if (!mushi?.submitFeedbackSignal || !selectedReportId) return;
364
+ await mushi.submitFeedbackSignal(selectedReportId, signal);
365
+ await loadInbox();
366
+ await openThread(selectedReportId);
367
+ }, [mushi, selectedReportId, loadInbox, openThread]);
320
368
  const resetForm = useCallback(() => {
321
369
  setCategory(null);
322
370
  setDescription("");
@@ -388,13 +436,18 @@ var MushiBottomSheet = ({ visible, onClose }) => {
388
436
  if (!category || !description.trim() || !mushi) return;
389
437
  setPhase("sending");
390
438
  try {
391
- await mushi.submitReport({ category, description: description.trim() });
439
+ await mushi.submitReport({
440
+ category,
441
+ description: description.trim(),
442
+ screenshotDataUrl: screenshotDataUrl && screenshotAttached ? screenshotDataUrl : void 0
443
+ });
392
444
  setPhase("sent");
393
445
  setTimeout(handleClose, 1400);
394
446
  } catch {
395
447
  setPhase("form");
396
448
  }
397
449
  };
450
+ const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
398
451
  const colors = dark ? { bg: "#1c1c1e", text: "#f2f2f7", sub: "#8e8e93", card: "#2c2c2e", accent: "#0a84ff", border: "#38383a", backdrop: "rgba(0,0,0,0.6)" } : { bg: "#ffffff", text: "#1c1c1e", sub: "#8e8e93", card: "#f2f2f7", accent: "#007aff", border: "#e5e5ea", backdrop: "rgba(0,0,0,0.35)" };
399
452
  const canSubmit = !!category && description.trim().length > 0 && phase === "form";
400
453
  return /* @__PURE__ */ jsx(Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ jsxs(
@@ -419,7 +472,44 @@ var MushiBottomSheet = ({ visible, onClose }) => {
419
472
  ],
420
473
  children: [
421
474
  /* @__PURE__ */ jsx(View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ jsx(View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
422
- phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
475
+ /* @__PURE__ */ jsx(View, { style: s.tabRow, children: ["report", "inbox"].map((tab) => /* @__PURE__ */ jsx(
476
+ TouchableOpacity,
477
+ {
478
+ onPress: () => {
479
+ setSheetTab(tab);
480
+ if (tab === "inbox") void loadInbox();
481
+ else setSelectedReportId(null);
482
+ },
483
+ style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
484
+ children: /* @__PURE__ */ jsx(Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.accent : colors.sub }], children: tab === "report" ? "Report" : "Inbox" })
485
+ },
486
+ tab
487
+ )) }),
488
+ sheetTab === "inbox" ? /* @__PURE__ */ jsx(View, { style: s.body, children: inboxLoading ? /* @__PURE__ */ jsx(Text, { style: { color: colors.sub }, children: "Loading\u2026" }) : selectedReportId ? /* @__PURE__ */ jsxs(Fragment, { children: [
489
+ /* @__PURE__ */ jsx(TouchableOpacity, { onPress: () => setSelectedReportId(null), children: /* @__PURE__ */ jsx(Text, { style: { color: colors.accent, marginBottom: 8 }, children: "\u2190 Back" }) }),
490
+ threadComments.map((c) => /* @__PURE__ */ jsxs(View, { style: [s.threadBubble, { backgroundColor: colors.card }], children: [
491
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: c.author_kind }),
492
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.text }, children: c.body })
493
+ ] }, c.id)),
494
+ inboxReports.find((r) => r.id === selectedReportId)?.status === "fixed" && /* @__PURE__ */ jsxs(View, { style: s.verifyRow, children: [
495
+ /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ jsx(Text, { style: s.submitText, children: "Yes, fixed" }) }),
496
+ /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.border }], onPress: () => submitFeedback("not_fixed"), children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.text }], children: "Not fixed" }) })
497
+ ] }),
498
+ /* @__PURE__ */ jsx(
499
+ TextInput,
500
+ {
501
+ style: [s.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border, minHeight: 48 }],
502
+ placeholder: "Reply\u2026",
503
+ placeholderTextColor: colors.sub,
504
+ value: replyText,
505
+ onChangeText: setReplyText
506
+ }
507
+ ),
508
+ /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ jsx(Text, { style: s.submitText, children: "Send" }) })
509
+ ] }) : inboxReports.map((r) => /* @__PURE__ */ jsxs(TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
510
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.text, fontWeight: "600" }, numberOfLines: 1, children: (r.summary ?? r.description ?? "Report").slice(0, 60) }),
511
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: r.status })
512
+ ] }, r.id)) }) : phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
423
513
  /* @__PURE__ */ jsx(Text, { style: [s.sentEmoji], children: "\u2705" }),
424
514
  /* @__PURE__ */ jsx(Text, { style: [s.sentText, { color: colors.text }], children: "Report sent!" })
425
515
  ] }) : /* @__PURE__ */ jsxs(View, { style: s.body, children: [
@@ -475,6 +565,32 @@ var MushiBottomSheet = ({ visible, onClose }) => {
475
565
  editable: phase === "form"
476
566
  }
477
567
  ),
568
+ activeScreenshot ? /* @__PURE__ */ jsxs(View, { style: s.screenshotRow, children: [
569
+ /* @__PURE__ */ jsx(
570
+ Image,
571
+ {
572
+ source: { uri: activeScreenshot },
573
+ style: s.screenshotThumb,
574
+ accessibilityLabel: "Attached screenshot"
575
+ }
576
+ ),
577
+ /* @__PURE__ */ jsxs(View, { style: s.screenshotMeta, children: [
578
+ /* @__PURE__ */ jsx(Text, { style: [s.screenshotLabel, { color: colors.text }], children: "Screenshot attached" }),
579
+ /* @__PURE__ */ jsx(Text, { style: [s.screenshotSub, { color: colors.sub }], children: "Helps the team see the issue" })
580
+ ] }),
581
+ /* @__PURE__ */ jsx(
582
+ TouchableOpacity,
583
+ {
584
+ onPress: () => {
585
+ setScreenshotAttached(false);
586
+ onClearScreenshot?.();
587
+ },
588
+ hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
589
+ accessibilityLabel: "Remove screenshot",
590
+ children: /* @__PURE__ */ jsx(Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
591
+ }
592
+ )
593
+ ] }) : null,
478
594
  /* @__PURE__ */ jsx(
479
595
  TouchableOpacity,
480
596
  {
@@ -587,6 +703,76 @@ var s = StyleSheet.create({
587
703
  sentText: {
588
704
  fontSize: 20,
589
705
  fontWeight: "600"
706
+ },
707
+ screenshotRow: {
708
+ flexDirection: "row",
709
+ alignItems: "center",
710
+ gap: 10,
711
+ marginBottom: 12,
712
+ paddingHorizontal: 10,
713
+ paddingVertical: 8,
714
+ borderRadius: 10,
715
+ backgroundColor: "rgba(128,128,128,0.1)"
716
+ },
717
+ screenshotThumb: {
718
+ width: 48,
719
+ height: 36,
720
+ borderRadius: 4,
721
+ backgroundColor: "#000",
722
+ flexShrink: 0
723
+ },
724
+ screenshotMeta: {
725
+ flex: 1,
726
+ gap: 2
727
+ },
728
+ screenshotLabel: {
729
+ fontSize: 13,
730
+ fontWeight: "600"
731
+ },
732
+ screenshotSub: {
733
+ fontSize: 11
734
+ },
735
+ screenshotRemove: {
736
+ fontSize: 16,
737
+ fontWeight: "700",
738
+ paddingHorizontal: 4
739
+ },
740
+ tabRow: {
741
+ flexDirection: "row",
742
+ borderBottomWidth: StyleSheet.hairlineWidth,
743
+ borderBottomColor: "#ccc",
744
+ marginBottom: 8
745
+ },
746
+ tabBtn: {
747
+ flex: 1,
748
+ alignItems: "center",
749
+ paddingVertical: 10,
750
+ borderBottomWidth: 2,
751
+ borderBottomColor: "transparent"
752
+ },
753
+ tabLabel: {
754
+ fontSize: 14,
755
+ fontWeight: "600"
756
+ },
757
+ inboxRow: {
758
+ paddingVertical: 12,
759
+ borderBottomWidth: StyleSheet.hairlineWidth
760
+ },
761
+ threadBubble: {
762
+ borderRadius: 10,
763
+ padding: 10,
764
+ marginBottom: 8
765
+ },
766
+ verifyRow: {
767
+ flexDirection: "row",
768
+ gap: 8,
769
+ marginBottom: 12
770
+ },
771
+ verifyBtn: {
772
+ flex: 1,
773
+ borderRadius: 10,
774
+ paddingVertical: 10,
775
+ alignItems: "center"
590
776
  }
591
777
  });
592
778
 
@@ -678,6 +864,15 @@ function MushiProvider({ children, ...config }) {
678
864
  const queueRef = useRef3(null);
679
865
  const apiClientRef = useRef3(null);
680
866
  const [sheetVisible, setSheetVisible] = useState2(false);
867
+ const [sheetScreenshot, setSheetScreenshot] = useState2(null);
868
+ const reporterTokenRef = useRef3(`rn-${config.projectId}-anon`);
869
+ let resolveReporterTokenReady = () => {
870
+ };
871
+ const reporterTokenReadyRef = useRef3(
872
+ new Promise((resolve) => {
873
+ resolveReporterTokenReady = resolve;
874
+ })
875
+ );
681
876
  if (config.endpoint !== void 0 && config.endpoint.trim() === "") {
682
877
  throw new Error(
683
878
  "[MushiProvider] endpoint is set to an empty string. Set endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api, or omit it to use the cloud default."
@@ -703,6 +898,27 @@ function MushiProvider({ children, ...config }) {
703
898
  });
704
899
  queueRef.current.flush().catch(() => {
705
900
  });
901
+ (async () => {
902
+ const TOKEN_KEY = "@mushi:reporter_token";
903
+ try {
904
+ const AsyncStorage = (await import("@react-native-async-storage/async-storage")).default;
905
+ const existing = await AsyncStorage.getItem(TOKEN_KEY);
906
+ if (existing && existing.startsWith("mushi_")) {
907
+ reporterTokenRef.current = existing;
908
+ } else {
909
+ const fresh = `mushi_${newUuid()}`;
910
+ reporterTokenRef.current = fresh;
911
+ await AsyncStorage.setItem(TOKEN_KEY, fresh);
912
+ }
913
+ resolveReporterTokenReady();
914
+ } catch {
915
+ if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
916
+ reporterTokenRef.current = `mushi_${newUuid()}`;
917
+ }
918
+ resolveReporterTokenReady();
919
+ }
920
+ })().catch(() => {
921
+ });
706
922
  return () => {
707
923
  consoleRef.current?.restore();
708
924
  networkRef.current?.restore();
@@ -722,7 +938,25 @@ function MushiProvider({ children, ...config }) {
722
938
  }
723
939
  return () => unsubscribe?.();
724
940
  }, []);
725
- const open = useCallback3(() => setSheetVisible(true), []);
941
+ const open = useCallback3(() => {
942
+ let vshot = null;
943
+ try {
944
+ vshot = __require("react-native-view-shot");
945
+ } catch {
946
+ }
947
+ if (!vshot) {
948
+ setSheetScreenshot(null);
949
+ setSheetVisible(true);
950
+ return;
951
+ }
952
+ vshot.captureScreen({ format: "jpg", quality: 0.7, result: "data-uri" }).then((url) => {
953
+ setSheetScreenshot(url);
954
+ }).catch(() => {
955
+ setSheetScreenshot(null);
956
+ }).finally(() => {
957
+ setSheetVisible(true);
958
+ });
959
+ }, []);
726
960
  const close = useCallback3(() => setSheetVisible(false), []);
727
961
  const attachTo = useCallback3(() => ({ onPress: open }), [open]);
728
962
  useEffect2(() => {
@@ -756,8 +990,16 @@ function MushiProvider({ children, ...config }) {
756
990
  const submitReport = useCallback3(
757
991
  async (data) => {
758
992
  const deviceInfo = getDeviceInfo();
993
+ const contextMeta = {};
994
+ if (userRef.current?.id) contextMeta.userId = userRef.current.id;
995
+ if (userRef.current?.email) contextMeta.userEmail = userRef.current.email;
996
+ if (userRef.current?.name) contextMeta.userName = userRef.current.name;
997
+ if (screenRef.current?.name) contextMeta.screenName = screenRef.current.name;
998
+ if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
999
+ if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
1000
+ const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
759
1001
  const report = {
760
- id: `rn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1002
+ id: newUuid(),
761
1003
  projectId: config.projectId,
762
1004
  category: data.category,
763
1005
  description: data.description,
@@ -766,7 +1008,7 @@ function MushiProvider({ children, ...config }) {
766
1008
  platform: deviceInfo.platform ?? "mobile",
767
1009
  language: deviceInfo.locale ?? "en",
768
1010
  viewport: { width: deviceInfo.screenWidth ?? 0, height: deviceInfo.screenHeight ?? 0 },
769
- url: "",
1011
+ url: screenRef.current?.route ?? "",
770
1012
  referrer: "",
771
1013
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
772
1014
  timezone: deviceInfo.timezone ?? "UTC"
@@ -776,7 +1018,9 @@ function MushiProvider({ children, ...config }) {
776
1018
  ...entry,
777
1019
  status: entry.status ?? 0
778
1020
  })) ?? [],
779
- reporterToken: `rn-${config.projectId}-anon`,
1021
+ screenshotDataUrl: data.screenshotDataUrl,
1022
+ metadata: merged,
1023
+ reporterToken: reporterTokenRef.current,
780
1024
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
781
1025
  };
782
1026
  const client = apiClientRef.current;
@@ -786,7 +1030,7 @@ function MushiProvider({ children, ...config }) {
786
1030
  await queueRef.current?.enqueue(report);
787
1031
  }
788
1032
  },
789
- [config.projectId, config.apiKey, apiEndpoint]
1033
+ [config.projectId, apiEndpoint]
790
1034
  );
791
1035
  const userRef = useRef3(null);
792
1036
  const metadataRef = useRef3({});
@@ -837,6 +1081,45 @@ function MushiProvider({ children, ...config }) {
837
1081
  setScreen(screen) {
838
1082
  screenRef.current = screen;
839
1083
  },
1084
+ // Reporter API — scoped to the device's persistent reporter token
1085
+ async listMyReports() {
1086
+ const client = apiClientRef.current;
1087
+ if (!client) return [];
1088
+ await reporterTokenReadyRef.current;
1089
+ const res = await client.listReporterReports(reporterTokenRef.current);
1090
+ return res.ok ? res.data?.reports ?? [] : [];
1091
+ },
1092
+ async listMyComments(reportId) {
1093
+ const client = apiClientRef.current;
1094
+ if (!client) return [];
1095
+ const res = await client.listReporterComments(reportId, reporterTokenRef.current);
1096
+ return res.ok ? res.data.comments ?? [] : [];
1097
+ },
1098
+ async replyToReport(reportId, body) {
1099
+ const client = apiClientRef.current;
1100
+ if (!client) return null;
1101
+ const res = await client.replyToReporterReport(reportId, reporterTokenRef.current, body);
1102
+ return res.ok ? res.data.comment : null;
1103
+ },
1104
+ async submitFeedbackSignal(reportId, signal, note) {
1105
+ const client = apiClientRef.current;
1106
+ if (!client) return null;
1107
+ const res = await client.replyToReporterReport(reportId, reporterTokenRef.current, note ?? "", signal);
1108
+ return res.ok ? res.data.feedback ?? null : null;
1109
+ },
1110
+ async reopenReport(reportId, note) {
1111
+ const client = apiClientRef.current;
1112
+ if (!client) return null;
1113
+ const res = await client.reopenReporterReport(reportId, reporterTokenRef.current, note);
1114
+ return res.ok ? res.data.outcome : null;
1115
+ },
1116
+ // Leaderboard — SDK-public anonymized hall-of-fame
1117
+ async getHallOfFame(limit = 10) {
1118
+ const client = apiClientRef.current;
1119
+ if (!client) return [];
1120
+ const res = await client.getHallOfFame(limit);
1121
+ return res.ok ? res.data.data ?? [] : [];
1122
+ },
840
1123
  // Rewards (P1)
841
1124
  recordActivity(action, metadata) {
842
1125
  if (!config.rewards?.enabled) return;
@@ -878,7 +1161,15 @@ function MushiProvider({ children, ...config }) {
878
1161
  inset: config.widget?.inset
879
1162
  }
880
1163
  ),
881
- /* @__PURE__ */ jsx3(MushiBottomSheet, { visible: sheetVisible, onClose: close })
1164
+ /* @__PURE__ */ jsx3(
1165
+ MushiBottomSheet,
1166
+ {
1167
+ visible: sheetVisible,
1168
+ onClose: close,
1169
+ screenshotDataUrl: sheetScreenshot ?? void 0,
1170
+ onClearScreenshot: () => setSheetScreenshot(null)
1171
+ }
1172
+ )
882
1173
  ] });
883
1174
  }
884
1175
  function useMushiContext() {
package/package.json CHANGED
@@ -1,23 +1,19 @@
1
1
  {
2
2
  "name": "@mushi-mushi/react-native",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "license": "MIT",
5
5
  "description": "React Native SDK for Mushi Mushi — shake-to-report, bottom sheet widget, full context capture",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
- "scripts": {
10
- "build": "tsup",
11
- "test": "vitest run",
12
- "typecheck": "tsc --noEmit"
13
- },
14
9
  "peerDependencies": {
15
10
  "@react-native-async-storage/async-storage": ">=1.19",
16
11
  "@react-native-community/netinfo": ">=11",
17
12
  "@react-navigation/native": ">=6",
18
13
  "expo-sensors": ">=13",
19
14
  "react": ">=18",
20
- "react-native": ">=0.72"
15
+ "react-native": ">=0.72",
16
+ "react-native-view-shot": ">=3.8"
21
17
  },
22
18
  "peerDependenciesMeta": {
23
19
  "@react-navigation/native": {
@@ -31,10 +27,13 @@
31
27
  },
32
28
  "@react-native-community/netinfo": {
33
29
  "optional": true
30
+ },
31
+ "react-native-view-shot": {
32
+ "optional": true
34
33
  }
35
34
  },
36
35
  "dependencies": {
37
- "@mushi-mushi/core": "workspace:^"
36
+ "@mushi-mushi/core": "^1.11.0"
38
37
  },
39
38
  "devDependencies": {
40
39
  "@types/react": "^19.2.14",
@@ -104,5 +103,10 @@
104
103
  "type": "module",
105
104
  "engines": {
106
105
  "node": ">=20"
106
+ },
107
+ "scripts": {
108
+ "build": "tsup",
109
+ "test": "vitest run",
110
+ "typecheck": "tsc --noEmit"
107
111
  }
108
- }
112
+ }