@mushi-mushi/react-native 0.14.0 → 0.15.1

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.cjs CHANGED
@@ -170,10 +170,12 @@ var AsyncStorageQueue = class {
170
170
  maxSize;
171
171
  apiEndpoint;
172
172
  apiKey;
173
+ onSynced;
173
174
  constructor(config) {
174
175
  this.maxSize = config.maxSize ?? 50;
175
176
  this.apiEndpoint = config.apiEndpoint;
176
177
  this.apiKey = config.apiKey;
178
+ this.onSynced = config.onSynced;
177
179
  }
178
180
  async enqueue(report) {
179
181
  const AsyncStorage = await this.getAsyncStorage();
@@ -195,6 +197,13 @@ var AsyncStorageQueue = class {
195
197
  const ok = await sendWithRetry(item, this.apiEndpoint, this.apiKey);
196
198
  if (ok) {
197
199
  flushed++;
200
+ const reportId = item.report.id;
201
+ if (typeof reportId === "string") {
202
+ try {
203
+ this.onSynced?.(reportId);
204
+ } catch {
205
+ }
206
+ }
198
207
  } else {
199
208
  remaining.push(item);
200
209
  break;
@@ -333,10 +342,47 @@ var MushiBottomSheet = ({
333
342
  const [category, setCategory] = (0, import_react.useState)(null);
334
343
  const [description, setDescription] = (0, import_react.useState)("");
335
344
  const [phase, setPhase] = (0, import_react.useState)("form");
345
+ const [sheetTab, setSheetTab] = (0, import_react.useState)("report");
346
+ const [inboxReports, setInboxReports] = (0, import_react.useState)([]);
347
+ const [inboxLoading, setInboxLoading] = (0, import_react.useState)(false);
348
+ const [selectedReportId, setSelectedReportId] = (0, import_react.useState)(null);
349
+ const [threadComments, setThreadComments] = (0, import_react.useState)([]);
350
+ const [replyText, setReplyText] = (0, import_react.useState)("");
336
351
  const [screenshotAttached, setScreenshotAttached] = (0, import_react.useState)(true);
337
352
  (0, import_react.useEffect)(() => {
338
- if (visible) setScreenshotAttached(true);
339
- }, [visible]);
353
+ if (visible) {
354
+ setScreenshotAttached(true);
355
+ if (sheetTab === "inbox") void loadInbox();
356
+ }
357
+ }, [visible, sheetTab]);
358
+ const loadInbox = (0, import_react.useCallback)(async () => {
359
+ if (!mushi?.listMyReports) return;
360
+ setInboxLoading(true);
361
+ try {
362
+ const rows = await mushi.listMyReports();
363
+ setInboxReports(rows);
364
+ } finally {
365
+ setInboxLoading(false);
366
+ }
367
+ }, [mushi]);
368
+ const openThread = (0, import_react.useCallback)(async (reportId) => {
369
+ setSelectedReportId(reportId);
370
+ if (!mushi?.listMyComments) return;
371
+ const comments = await mushi.listMyComments(reportId);
372
+ setThreadComments(comments);
373
+ }, [mushi]);
374
+ const sendReply = (0, import_react.useCallback)(async () => {
375
+ if (!mushi?.replyToReport || !selectedReportId || !replyText.trim()) return;
376
+ await mushi.replyToReport(selectedReportId, replyText.trim());
377
+ setReplyText("");
378
+ await openThread(selectedReportId);
379
+ }, [mushi, selectedReportId, replyText, openThread]);
380
+ const submitFeedback = (0, import_react.useCallback)(async (signal) => {
381
+ if (!mushi?.submitFeedbackSignal || !selectedReportId) return;
382
+ await mushi.submitFeedbackSignal(selectedReportId, signal);
383
+ await loadInbox();
384
+ await openThread(selectedReportId);
385
+ }, [mushi, selectedReportId, loadInbox, openThread]);
340
386
  const resetForm = (0, import_react.useCallback)(() => {
341
387
  setCategory(null);
342
388
  setDescription("");
@@ -444,7 +490,44 @@ var MushiBottomSheet = ({
444
490
  ],
445
491
  children: [
446
492
  /* @__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 }] }) }),
447
- phase === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.sentWrap, children: [
493
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.tabRow, children: ["report", "inbox"].map((tab) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
494
+ import_react_native2.TouchableOpacity,
495
+ {
496
+ onPress: () => {
497
+ setSheetTab(tab);
498
+ if (tab === "inbox") void loadInbox();
499
+ else setSelectedReportId(null);
500
+ },
501
+ style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
502
+ 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" })
503
+ },
504
+ tab
505
+ )) }),
506
+ 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: [
507
+ /* @__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" }) }),
508
+ threadComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: [s.threadBubble, { backgroundColor: colors.card }], children: [
509
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, fontSize: 11 }, children: c.author_kind }),
510
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.text }, children: c.body })
511
+ ] }, c.id)),
512
+ inboxReports.find((r) => r.id === selectedReportId)?.status === "fixed" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.verifyRow, children: [
513
+ /* @__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" }) }),
514
+ /* @__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" }) })
515
+ ] }),
516
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
517
+ import_react_native2.TextInput,
518
+ {
519
+ style: [s.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border, minHeight: 48 }],
520
+ placeholder: "Reply\u2026",
521
+ placeholderTextColor: colors.sub,
522
+ value: replyText,
523
+ onChangeText: setReplyText
524
+ }
525
+ ),
526
+ /* @__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" }) })
527
+ ] }) : inboxReports.map((r) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
528
+ /* @__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) }),
529
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, fontSize: 11 }, children: r.status })
530
+ ] }, r.id)) }) : phase === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.sentWrap, children: [
448
531
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentEmoji], children: "\u2705" }),
449
532
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentText, { color: colors.text }], children: "Report sent!" })
450
533
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.body, children: [
@@ -671,6 +754,43 @@ var s = import_react_native2.StyleSheet.create({
671
754
  fontSize: 16,
672
755
  fontWeight: "700",
673
756
  paddingHorizontal: 4
757
+ },
758
+ tabRow: {
759
+ flexDirection: "row",
760
+ borderBottomWidth: import_react_native2.StyleSheet.hairlineWidth,
761
+ borderBottomColor: "#ccc",
762
+ marginBottom: 8
763
+ },
764
+ tabBtn: {
765
+ flex: 1,
766
+ alignItems: "center",
767
+ paddingVertical: 10,
768
+ borderBottomWidth: 2,
769
+ borderBottomColor: "transparent"
770
+ },
771
+ tabLabel: {
772
+ fontSize: 14,
773
+ fontWeight: "600"
774
+ },
775
+ inboxRow: {
776
+ paddingVertical: 12,
777
+ borderBottomWidth: import_react_native2.StyleSheet.hairlineWidth
778
+ },
779
+ threadBubble: {
780
+ borderRadius: 10,
781
+ padding: 10,
782
+ marginBottom: 8
783
+ },
784
+ verifyRow: {
785
+ flexDirection: "row",
786
+ gap: 8,
787
+ marginBottom: 12
788
+ },
789
+ verifyBtn: {
790
+ flex: 1,
791
+ borderRadius: 10,
792
+ paddingVertical: 10,
793
+ alignItems: "center"
674
794
  }
675
795
  });
676
796
 
@@ -765,12 +885,16 @@ function MushiProvider({ children, ...config }) {
765
885
  resolveReporterTokenReady = resolve;
766
886
  })
767
887
  );
768
- if (config.endpoint !== void 0 && config.endpoint.trim() === "") {
888
+ const rawEndpoint = config.apiEndpoint ?? config.endpoint;
889
+ if (rawEndpoint !== void 0 && rawEndpoint.trim() === "") {
769
890
  throw new Error(
770
- "[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."
891
+ "[MushiProvider] apiEndpoint is set to an empty string. Set apiEndpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api, or omit it to use the cloud default."
771
892
  );
772
893
  }
773
- const apiEndpoint = config.endpoint ?? import_core.DEFAULT_API_ENDPOINT;
894
+ if (config.endpoint && !config.apiEndpoint && __DEV__) {
895
+ console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
896
+ }
897
+ const apiEndpoint = rawEndpoint ?? import_core.DEFAULT_API_ENDPOINT;
774
898
  (0, import_react3.useEffect)(() => {
775
899
  if (config.capture?.console !== false) {
776
900
  consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
@@ -786,7 +910,11 @@ function MushiProvider({ children, ...config }) {
786
910
  queueRef.current = new AsyncStorageQueue({
787
911
  maxSize: config.storage?.maxQueueSize,
788
912
  apiEndpoint,
789
- apiKey: config.apiKey
913
+ apiKey: config.apiKey,
914
+ // `onReportSynced` fires here — when a previously-queued report actually
915
+ // drains to the server — not on the direct-submit path (where it would be
916
+ // indistinguishable from `onReportSubmitted` and thus meaningless).
917
+ onSynced: (reportId) => config.callbacks?.onReportSynced?.(reportId)
790
918
  });
791
919
  queueRef.current.flush().catch(() => {
792
920
  });
@@ -920,9 +1048,12 @@ function MushiProvider({ children, ...config }) {
920
1048
  const result = await client.submitReport(report);
921
1049
  if (!result.ok) {
922
1050
  await queueRef.current?.enqueue(report);
1051
+ config.callbacks?.onReportQueued?.(report.id);
1052
+ } else {
1053
+ config.callbacks?.onReportSubmitted?.(report.id);
923
1054
  }
924
1055
  },
925
- [config.projectId, apiEndpoint]
1056
+ [config.projectId, config.callbacks, apiEndpoint]
926
1057
  );
927
1058
  const userRef = (0, import_react3.useRef)(null);
928
1059
  const metadataRef = (0, import_react3.useRef)({});
@@ -993,6 +1124,18 @@ function MushiProvider({ children, ...config }) {
993
1124
  const res = await client.replyToReporterReport(reportId, reporterTokenRef.current, body);
994
1125
  return res.ok ? res.data.comment : null;
995
1126
  },
1127
+ async submitFeedbackSignal(reportId, signal, note) {
1128
+ const client = apiClientRef.current;
1129
+ if (!client) return null;
1130
+ const res = await client.replyToReporterReport(reportId, reporterTokenRef.current, note ?? "", signal);
1131
+ return res.ok ? res.data.feedback ?? null : null;
1132
+ },
1133
+ async reopenReport(reportId, note) {
1134
+ const client = apiClientRef.current;
1135
+ if (!client) return null;
1136
+ const res = await client.reopenReporterReport(reportId, reporterTokenRef.current, note);
1137
+ return res.ok ? res.data.outcome : null;
1138
+ },
996
1139
  // Leaderboard — SDK-public anonymized hall-of-fame
997
1140
  async getHallOfFame(limit = 10) {
998
1141
  const client = apiClientRef.current;
package/dist/index.d.cts CHANGED
@@ -46,7 +46,18 @@ declare function getDeviceInfo(): DeviceInfo;
46
46
  interface MushiRNConfig {
47
47
  projectId: string;
48
48
  apiKey: string;
49
+ /** @deprecated Prefer `apiEndpoint` — kept for backward compatibility */
49
50
  endpoint?: string;
51
+ /** Canonical Mushi API base URL (e.g. https://xyz.supabase.co/functions/v1/api) */
52
+ apiEndpoint?: string;
53
+ /** Lifecycle callbacks for host-app integration (replaces polling in HHTP etc.) */
54
+ callbacks?: {
55
+ onReportSubmitted?: (reportId: string) => void;
56
+ onReportQueued?: (reportId: string) => void;
57
+ onReportSynced?: (reportId: string) => void;
58
+ onReporterReply?: (reportId: string, commentId: string) => void;
59
+ onFixStatusChanged?: (reportId: string, status: string) => void;
60
+ };
50
61
  widget?: {
51
62
  trigger?: 'shake' | 'button' | 'both' | 'manual' | 'auto' | 'edge-tab' | 'hidden' | 'attach';
52
63
  shakeThreshold?: number;
@@ -107,6 +118,10 @@ interface MushiRNInstance {
107
118
  listMyComments(reportId: string): Promise<MushiReporterComment[]>;
108
119
  /** Post a reporter reply on a report thread. Returns the new comment or null on failure. */
109
120
  replyToReport(reportId: string, body: string): Promise<MushiReporterComment | null>;
121
+ /** Record a reporter feedback signal (e.g. `confirms`, `not_fixed`) on a report. Returns the outcome or null. */
122
+ submitFeedbackSignal(reportId: string, signal: string, note?: string): Promise<Record<string, unknown> | null>;
123
+ /** Reopen a report the reporter flagged as not fixed. Returns the reopen outcome or null. */
124
+ reopenReport(reportId: string, note?: string): Promise<Record<string, unknown> | null>;
110
125
  /** Fetch the project's top contributors by points (max 50). */
111
126
  getHallOfFame(limit?: number): Promise<MushiHallOfFameEntry[]>;
112
127
  /** Manually record a host-defined activity event. */
@@ -236,10 +251,13 @@ declare class AsyncStorageQueue {
236
251
  private maxSize;
237
252
  private apiEndpoint;
238
253
  private apiKey;
254
+ private onSynced?;
239
255
  constructor(config: {
240
256
  maxSize?: number;
241
257
  apiEndpoint: string;
242
258
  apiKey: string;
259
+ /** Fired once per queued report that drains successfully to the server. */
260
+ onSynced?: (reportId: string) => void;
243
261
  });
244
262
  enqueue(report: object): Promise<void>;
245
263
  flush(): Promise<number>;
package/dist/index.d.ts CHANGED
@@ -46,7 +46,18 @@ declare function getDeviceInfo(): DeviceInfo;
46
46
  interface MushiRNConfig {
47
47
  projectId: string;
48
48
  apiKey: string;
49
+ /** @deprecated Prefer `apiEndpoint` — kept for backward compatibility */
49
50
  endpoint?: string;
51
+ /** Canonical Mushi API base URL (e.g. https://xyz.supabase.co/functions/v1/api) */
52
+ apiEndpoint?: string;
53
+ /** Lifecycle callbacks for host-app integration (replaces polling in HHTP etc.) */
54
+ callbacks?: {
55
+ onReportSubmitted?: (reportId: string) => void;
56
+ onReportQueued?: (reportId: string) => void;
57
+ onReportSynced?: (reportId: string) => void;
58
+ onReporterReply?: (reportId: string, commentId: string) => void;
59
+ onFixStatusChanged?: (reportId: string, status: string) => void;
60
+ };
50
61
  widget?: {
51
62
  trigger?: 'shake' | 'button' | 'both' | 'manual' | 'auto' | 'edge-tab' | 'hidden' | 'attach';
52
63
  shakeThreshold?: number;
@@ -107,6 +118,10 @@ interface MushiRNInstance {
107
118
  listMyComments(reportId: string): Promise<MushiReporterComment[]>;
108
119
  /** Post a reporter reply on a report thread. Returns the new comment or null on failure. */
109
120
  replyToReport(reportId: string, body: string): Promise<MushiReporterComment | null>;
121
+ /** Record a reporter feedback signal (e.g. `confirms`, `not_fixed`) on a report. Returns the outcome or null. */
122
+ submitFeedbackSignal(reportId: string, signal: string, note?: string): Promise<Record<string, unknown> | null>;
123
+ /** Reopen a report the reporter flagged as not fixed. Returns the reopen outcome or null. */
124
+ reopenReport(reportId: string, note?: string): Promise<Record<string, unknown> | null>;
110
125
  /** Fetch the project's top contributors by points (max 50). */
111
126
  getHallOfFame(limit?: number): Promise<MushiHallOfFameEntry[]>;
112
127
  /** Manually record a host-defined activity event. */
@@ -236,10 +251,13 @@ declare class AsyncStorageQueue {
236
251
  private maxSize;
237
252
  private apiEndpoint;
238
253
  private apiKey;
254
+ private onSynced?;
239
255
  constructor(config: {
240
256
  maxSize?: number;
241
257
  apiEndpoint: string;
242
258
  apiKey: string;
259
+ /** Fired once per queued report that drains successfully to the server. */
260
+ onSynced?: (reportId: string) => void;
243
261
  });
244
262
  enqueue(report: object): Promise<void>;
245
263
  flush(): Promise<number>;
package/dist/index.js CHANGED
@@ -142,10 +142,12 @@ var AsyncStorageQueue = class {
142
142
  maxSize;
143
143
  apiEndpoint;
144
144
  apiKey;
145
+ onSynced;
145
146
  constructor(config) {
146
147
  this.maxSize = config.maxSize ?? 50;
147
148
  this.apiEndpoint = config.apiEndpoint;
148
149
  this.apiKey = config.apiKey;
150
+ this.onSynced = config.onSynced;
149
151
  }
150
152
  async enqueue(report) {
151
153
  const AsyncStorage = await this.getAsyncStorage();
@@ -167,6 +169,13 @@ var AsyncStorageQueue = class {
167
169
  const ok = await sendWithRetry(item, this.apiEndpoint, this.apiKey);
168
170
  if (ok) {
169
171
  flushed++;
172
+ const reportId = item.report.id;
173
+ if (typeof reportId === "string") {
174
+ try {
175
+ this.onSynced?.(reportId);
176
+ } catch {
177
+ }
178
+ }
170
179
  } else {
171
180
  remaining.push(item);
172
181
  break;
@@ -299,7 +308,7 @@ import {
299
308
  KeyboardAvoidingView,
300
309
  useColorScheme
301
310
  } from "react-native";
302
- import { jsx, jsxs } from "react/jsx-runtime";
311
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
303
312
  var { height: SCREEN_HEIGHT } = Dimensions2.get("window");
304
313
  var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
305
314
  var DISMISS_THRESHOLD = 80;
@@ -324,10 +333,47 @@ var MushiBottomSheet = ({
324
333
  const [category, setCategory] = useState(null);
325
334
  const [description, setDescription] = useState("");
326
335
  const [phase, setPhase] = useState("form");
336
+ const [sheetTab, setSheetTab] = useState("report");
337
+ const [inboxReports, setInboxReports] = useState([]);
338
+ const [inboxLoading, setInboxLoading] = useState(false);
339
+ const [selectedReportId, setSelectedReportId] = useState(null);
340
+ const [threadComments, setThreadComments] = useState([]);
341
+ const [replyText, setReplyText] = useState("");
327
342
  const [screenshotAttached, setScreenshotAttached] = useState(true);
328
343
  useEffect(() => {
329
- if (visible) setScreenshotAttached(true);
330
- }, [visible]);
344
+ if (visible) {
345
+ setScreenshotAttached(true);
346
+ if (sheetTab === "inbox") void loadInbox();
347
+ }
348
+ }, [visible, sheetTab]);
349
+ const loadInbox = 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 = 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 = 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 = 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 = useCallback(() => {
332
378
  setCategory(null);
333
379
  setDescription("");
@@ -435,7 +481,44 @@ var MushiBottomSheet = ({
435
481
  ],
436
482
  children: [
437
483
  /* @__PURE__ */ jsx(View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ jsx(View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
438
- phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
484
+ /* @__PURE__ */ jsx(View, { style: s.tabRow, children: ["report", "inbox"].map((tab) => /* @__PURE__ */ jsx(
485
+ 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__ */ jsx(Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.accent : colors.sub }], children: tab === "report" ? "Report" : "Inbox" })
494
+ },
495
+ tab
496
+ )) }),
497
+ 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: [
498
+ /* @__PURE__ */ jsx(TouchableOpacity, { onPress: () => setSelectedReportId(null), children: /* @__PURE__ */ jsx(Text, { style: { color: colors.accent, marginBottom: 8 }, children: "\u2190 Back" }) }),
499
+ threadComments.map((c) => /* @__PURE__ */ jsxs(View, { style: [s.threadBubble, { backgroundColor: colors.card }], children: [
500
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: c.author_kind }),
501
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.text }, children: c.body })
502
+ ] }, c.id)),
503
+ inboxReports.find((r) => r.id === selectedReportId)?.status === "fixed" && /* @__PURE__ */ jsxs(View, { style: s.verifyRow, children: [
504
+ /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ jsx(Text, { style: s.submitText, children: "Yes, fixed" }) }),
505
+ /* @__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" }) })
506
+ ] }),
507
+ /* @__PURE__ */ jsx(
508
+ 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__ */ jsx(TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ jsx(Text, { style: s.submitText, children: "Send" }) })
518
+ ] }) : inboxReports.map((r) => /* @__PURE__ */ jsxs(TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
519
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.text, fontWeight: "600" }, numberOfLines: 1, children: (r.summary ?? r.description ?? "Report").slice(0, 60) }),
520
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: r.status })
521
+ ] }, r.id)) }) : phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
439
522
  /* @__PURE__ */ jsx(Text, { style: [s.sentEmoji], children: "\u2705" }),
440
523
  /* @__PURE__ */ jsx(Text, { style: [s.sentText, { color: colors.text }], children: "Report sent!" })
441
524
  ] }) : /* @__PURE__ */ jsxs(View, { style: s.body, children: [
@@ -662,6 +745,43 @@ var s = StyleSheet.create({
662
745
  fontSize: 16,
663
746
  fontWeight: "700",
664
747
  paddingHorizontal: 4
748
+ },
749
+ tabRow: {
750
+ flexDirection: "row",
751
+ borderBottomWidth: 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: 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"
665
785
  }
666
786
  });
667
787
 
@@ -762,12 +882,16 @@ function MushiProvider({ children, ...config }) {
762
882
  resolveReporterTokenReady = resolve;
763
883
  })
764
884
  );
765
- if (config.endpoint !== void 0 && config.endpoint.trim() === "") {
885
+ const rawEndpoint = config.apiEndpoint ?? config.endpoint;
886
+ if (rawEndpoint !== void 0 && rawEndpoint.trim() === "") {
766
887
  throw new Error(
767
- "[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."
888
+ "[MushiProvider] apiEndpoint is set to an empty string. Set apiEndpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api, or omit it to use the cloud default."
768
889
  );
769
890
  }
770
- const apiEndpoint = config.endpoint ?? DEFAULT_API_ENDPOINT;
891
+ if (config.endpoint && !config.apiEndpoint && __DEV__) {
892
+ console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
893
+ }
894
+ const apiEndpoint = rawEndpoint ?? DEFAULT_API_ENDPOINT;
771
895
  useEffect2(() => {
772
896
  if (config.capture?.console !== false) {
773
897
  consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
@@ -783,7 +907,11 @@ function MushiProvider({ children, ...config }) {
783
907
  queueRef.current = new AsyncStorageQueue({
784
908
  maxSize: config.storage?.maxQueueSize,
785
909
  apiEndpoint,
786
- apiKey: config.apiKey
910
+ apiKey: config.apiKey,
911
+ // `onReportSynced` fires here — when a previously-queued report actually
912
+ // drains to the server — not on the direct-submit path (where it would be
913
+ // indistinguishable from `onReportSubmitted` and thus meaningless).
914
+ onSynced: (reportId) => config.callbacks?.onReportSynced?.(reportId)
787
915
  });
788
916
  queueRef.current.flush().catch(() => {
789
917
  });
@@ -917,9 +1045,12 @@ function MushiProvider({ children, ...config }) {
917
1045
  const result = await client.submitReport(report);
918
1046
  if (!result.ok) {
919
1047
  await queueRef.current?.enqueue(report);
1048
+ config.callbacks?.onReportQueued?.(report.id);
1049
+ } else {
1050
+ config.callbacks?.onReportSubmitted?.(report.id);
920
1051
  }
921
1052
  },
922
- [config.projectId, apiEndpoint]
1053
+ [config.projectId, config.callbacks, apiEndpoint]
923
1054
  );
924
1055
  const userRef = useRef3(null);
925
1056
  const metadataRef = useRef3({});
@@ -990,6 +1121,18 @@ function MushiProvider({ children, ...config }) {
990
1121
  const res = await client.replyToReporterReport(reportId, reporterTokenRef.current, body);
991
1122
  return res.ok ? res.data.comment : null;
992
1123
  },
1124
+ async submitFeedbackSignal(reportId, signal, note) {
1125
+ const client = apiClientRef.current;
1126
+ if (!client) return null;
1127
+ const res = await client.replyToReporterReport(reportId, reporterTokenRef.current, note ?? "", signal);
1128
+ return res.ok ? res.data.feedback ?? null : null;
1129
+ },
1130
+ async reopenReport(reportId, note) {
1131
+ const client = apiClientRef.current;
1132
+ if (!client) return null;
1133
+ const res = await client.reopenReporterReport(reportId, reporterTokenRef.current, note);
1134
+ return res.ok ? res.data.outcome : null;
1135
+ },
993
1136
  // Leaderboard — SDK-public anonymized hall-of-fame
994
1137
  async getHallOfFame(limit = 10) {
995
1138
  const client = apiClientRef.current;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mushi-mushi/react-native",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
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",
@@ -33,7 +33,7 @@
33
33
  }
34
34
  },
35
35
  "dependencies": {
36
- "@mushi-mushi/core": "^1.10.0"
36
+ "@mushi-mushi/core": "^1.12.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/react": "^19.2.14",