@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 +151 -8
- package/dist/index.d.cts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +152 -9
- package/package.json +2 -2
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)
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
888
|
+
const rawEndpoint = config.apiEndpoint ?? config.endpoint;
|
|
889
|
+
if (rawEndpoint !== void 0 && rawEndpoint.trim() === "") {
|
|
769
890
|
throw new Error(
|
|
770
|
-
"[MushiProvider]
|
|
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
|
-
|
|
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)
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
885
|
+
const rawEndpoint = config.apiEndpoint ?? config.endpoint;
|
|
886
|
+
if (rawEndpoint !== void 0 && rawEndpoint.trim() === "") {
|
|
766
887
|
throw new Error(
|
|
767
|
-
"[MushiProvider]
|
|
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
|
-
|
|
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.
|
|
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.
|
|
36
|
+
"@mushi-mushi/core": "^1.12.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/react": "^19.2.14",
|