@mushi-mushi/react-native 0.13.1 → 0.14.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 +6 -0
- package/dist/index.cjs +174 -8
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +177 -9
- package/package.json +13 -9
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 = ({
|
|
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,10 @@ 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 [screenshotAttached, setScreenshotAttached] = (0, import_react.useState)(true);
|
|
337
|
+
(0, import_react.useEffect)(() => {
|
|
338
|
+
if (visible) setScreenshotAttached(true);
|
|
339
|
+
}, [visible]);
|
|
331
340
|
const resetForm = (0, import_react.useCallback)(() => {
|
|
332
341
|
setCategory(null);
|
|
333
342
|
setDescription("");
|
|
@@ -399,13 +408,18 @@ var MushiBottomSheet = ({ visible, onClose }) => {
|
|
|
399
408
|
if (!category || !description.trim() || !mushi) return;
|
|
400
409
|
setPhase("sending");
|
|
401
410
|
try {
|
|
402
|
-
await mushi.submitReport({
|
|
411
|
+
await mushi.submitReport({
|
|
412
|
+
category,
|
|
413
|
+
description: description.trim(),
|
|
414
|
+
screenshotDataUrl: screenshotDataUrl && screenshotAttached ? screenshotDataUrl : void 0
|
|
415
|
+
});
|
|
403
416
|
setPhase("sent");
|
|
404
417
|
setTimeout(handleClose, 1400);
|
|
405
418
|
} catch {
|
|
406
419
|
setPhase("form");
|
|
407
420
|
}
|
|
408
421
|
};
|
|
422
|
+
const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
|
|
409
423
|
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
424
|
const canSubmit = !!category && description.trim().length > 0 && phase === "form";
|
|
411
425
|
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)(
|
|
@@ -486,6 +500,32 @@ var MushiBottomSheet = ({ visible, onClose }) => {
|
|
|
486
500
|
editable: phase === "form"
|
|
487
501
|
}
|
|
488
502
|
),
|
|
503
|
+
activeScreenshot ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotRow, children: [
|
|
504
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
505
|
+
import_react_native2.Image,
|
|
506
|
+
{
|
|
507
|
+
source: { uri: activeScreenshot },
|
|
508
|
+
style: s.screenshotThumb,
|
|
509
|
+
accessibilityLabel: "Attached screenshot"
|
|
510
|
+
}
|
|
511
|
+
),
|
|
512
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotMeta, children: [
|
|
513
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotLabel, { color: colors.text }], children: "Screenshot attached" }),
|
|
514
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotSub, { color: colors.sub }], children: "Helps the team see the issue" })
|
|
515
|
+
] }),
|
|
516
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
517
|
+
import_react_native2.TouchableOpacity,
|
|
518
|
+
{
|
|
519
|
+
onPress: () => {
|
|
520
|
+
setScreenshotAttached(false);
|
|
521
|
+
onClearScreenshot?.();
|
|
522
|
+
},
|
|
523
|
+
hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
|
|
524
|
+
accessibilityLabel: "Remove screenshot",
|
|
525
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
] }) : null,
|
|
489
529
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
490
530
|
import_react_native2.TouchableOpacity,
|
|
491
531
|
{
|
|
@@ -598,6 +638,39 @@ var s = import_react_native2.StyleSheet.create({
|
|
|
598
638
|
sentText: {
|
|
599
639
|
fontSize: 20,
|
|
600
640
|
fontWeight: "600"
|
|
641
|
+
},
|
|
642
|
+
screenshotRow: {
|
|
643
|
+
flexDirection: "row",
|
|
644
|
+
alignItems: "center",
|
|
645
|
+
gap: 10,
|
|
646
|
+
marginBottom: 12,
|
|
647
|
+
paddingHorizontal: 10,
|
|
648
|
+
paddingVertical: 8,
|
|
649
|
+
borderRadius: 10,
|
|
650
|
+
backgroundColor: "rgba(128,128,128,0.1)"
|
|
651
|
+
},
|
|
652
|
+
screenshotThumb: {
|
|
653
|
+
width: 48,
|
|
654
|
+
height: 36,
|
|
655
|
+
borderRadius: 4,
|
|
656
|
+
backgroundColor: "#000",
|
|
657
|
+
flexShrink: 0
|
|
658
|
+
},
|
|
659
|
+
screenshotMeta: {
|
|
660
|
+
flex: 1,
|
|
661
|
+
gap: 2
|
|
662
|
+
},
|
|
663
|
+
screenshotLabel: {
|
|
664
|
+
fontSize: 13,
|
|
665
|
+
fontWeight: "600"
|
|
666
|
+
},
|
|
667
|
+
screenshotSub: {
|
|
668
|
+
fontSize: 11
|
|
669
|
+
},
|
|
670
|
+
screenshotRemove: {
|
|
671
|
+
fontSize: 16,
|
|
672
|
+
fontWeight: "700",
|
|
673
|
+
paddingHorizontal: 4
|
|
601
674
|
}
|
|
602
675
|
});
|
|
603
676
|
|
|
@@ -683,6 +756,15 @@ function MushiProvider({ children, ...config }) {
|
|
|
683
756
|
const queueRef = (0, import_react3.useRef)(null);
|
|
684
757
|
const apiClientRef = (0, import_react3.useRef)(null);
|
|
685
758
|
const [sheetVisible, setSheetVisible] = (0, import_react3.useState)(false);
|
|
759
|
+
const [sheetScreenshot, setSheetScreenshot] = (0, import_react3.useState)(null);
|
|
760
|
+
const reporterTokenRef = (0, import_react3.useRef)(`rn-${config.projectId}-anon`);
|
|
761
|
+
let resolveReporterTokenReady = () => {
|
|
762
|
+
};
|
|
763
|
+
const reporterTokenReadyRef = (0, import_react3.useRef)(
|
|
764
|
+
new Promise((resolve) => {
|
|
765
|
+
resolveReporterTokenReady = resolve;
|
|
766
|
+
})
|
|
767
|
+
);
|
|
686
768
|
if (config.endpoint !== void 0 && config.endpoint.trim() === "") {
|
|
687
769
|
throw new Error(
|
|
688
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."
|
|
@@ -708,6 +790,27 @@ function MushiProvider({ children, ...config }) {
|
|
|
708
790
|
});
|
|
709
791
|
queueRef.current.flush().catch(() => {
|
|
710
792
|
});
|
|
793
|
+
(async () => {
|
|
794
|
+
const TOKEN_KEY = "@mushi:reporter_token";
|
|
795
|
+
try {
|
|
796
|
+
const AsyncStorage = (await import("@react-native-async-storage/async-storage")).default;
|
|
797
|
+
const existing = await AsyncStorage.getItem(TOKEN_KEY);
|
|
798
|
+
if (existing && existing.startsWith("mushi_")) {
|
|
799
|
+
reporterTokenRef.current = existing;
|
|
800
|
+
} else {
|
|
801
|
+
const fresh = `mushi_${(0, import_core.newUuid)()}`;
|
|
802
|
+
reporterTokenRef.current = fresh;
|
|
803
|
+
await AsyncStorage.setItem(TOKEN_KEY, fresh);
|
|
804
|
+
}
|
|
805
|
+
resolveReporterTokenReady();
|
|
806
|
+
} catch {
|
|
807
|
+
if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
|
|
808
|
+
reporterTokenRef.current = `mushi_${(0, import_core.newUuid)()}`;
|
|
809
|
+
}
|
|
810
|
+
resolveReporterTokenReady();
|
|
811
|
+
}
|
|
812
|
+
})().catch(() => {
|
|
813
|
+
});
|
|
711
814
|
return () => {
|
|
712
815
|
consoleRef.current?.restore();
|
|
713
816
|
networkRef.current?.restore();
|
|
@@ -727,7 +830,25 @@ function MushiProvider({ children, ...config }) {
|
|
|
727
830
|
}
|
|
728
831
|
return () => unsubscribe?.();
|
|
729
832
|
}, []);
|
|
730
|
-
const open = (0, import_react3.useCallback)(() =>
|
|
833
|
+
const open = (0, import_react3.useCallback)(() => {
|
|
834
|
+
let vshot = null;
|
|
835
|
+
try {
|
|
836
|
+
vshot = require("react-native-view-shot");
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
if (!vshot) {
|
|
840
|
+
setSheetScreenshot(null);
|
|
841
|
+
setSheetVisible(true);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
vshot.captureScreen({ format: "jpg", quality: 0.7, result: "data-uri" }).then((url) => {
|
|
845
|
+
setSheetScreenshot(url);
|
|
846
|
+
}).catch(() => {
|
|
847
|
+
setSheetScreenshot(null);
|
|
848
|
+
}).finally(() => {
|
|
849
|
+
setSheetVisible(true);
|
|
850
|
+
});
|
|
851
|
+
}, []);
|
|
731
852
|
const close = (0, import_react3.useCallback)(() => setSheetVisible(false), []);
|
|
732
853
|
const attachTo = (0, import_react3.useCallback)(() => ({ onPress: open }), [open]);
|
|
733
854
|
(0, import_react3.useEffect)(() => {
|
|
@@ -761,8 +882,16 @@ function MushiProvider({ children, ...config }) {
|
|
|
761
882
|
const submitReport = (0, import_react3.useCallback)(
|
|
762
883
|
async (data) => {
|
|
763
884
|
const deviceInfo = getDeviceInfo();
|
|
885
|
+
const contextMeta = {};
|
|
886
|
+
if (userRef.current?.id) contextMeta.userId = userRef.current.id;
|
|
887
|
+
if (userRef.current?.email) contextMeta.userEmail = userRef.current.email;
|
|
888
|
+
if (userRef.current?.name) contextMeta.userName = userRef.current.name;
|
|
889
|
+
if (screenRef.current?.name) contextMeta.screenName = screenRef.current.name;
|
|
890
|
+
if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
|
|
891
|
+
if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
|
|
892
|
+
const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
|
|
764
893
|
const report = {
|
|
765
|
-
id:
|
|
894
|
+
id: (0, import_core.newUuid)(),
|
|
766
895
|
projectId: config.projectId,
|
|
767
896
|
category: data.category,
|
|
768
897
|
description: data.description,
|
|
@@ -771,7 +900,7 @@ function MushiProvider({ children, ...config }) {
|
|
|
771
900
|
platform: deviceInfo.platform ?? "mobile",
|
|
772
901
|
language: deviceInfo.locale ?? "en",
|
|
773
902
|
viewport: { width: deviceInfo.screenWidth ?? 0, height: deviceInfo.screenHeight ?? 0 },
|
|
774
|
-
url: "",
|
|
903
|
+
url: screenRef.current?.route ?? "",
|
|
775
904
|
referrer: "",
|
|
776
905
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
777
906
|
timezone: deviceInfo.timezone ?? "UTC"
|
|
@@ -781,7 +910,9 @@ function MushiProvider({ children, ...config }) {
|
|
|
781
910
|
...entry,
|
|
782
911
|
status: entry.status ?? 0
|
|
783
912
|
})) ?? [],
|
|
784
|
-
|
|
913
|
+
screenshotDataUrl: data.screenshotDataUrl,
|
|
914
|
+
metadata: merged,
|
|
915
|
+
reporterToken: reporterTokenRef.current,
|
|
785
916
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
786
917
|
};
|
|
787
918
|
const client = apiClientRef.current;
|
|
@@ -791,7 +922,7 @@ function MushiProvider({ children, ...config }) {
|
|
|
791
922
|
await queueRef.current?.enqueue(report);
|
|
792
923
|
}
|
|
793
924
|
},
|
|
794
|
-
[config.projectId,
|
|
925
|
+
[config.projectId, apiEndpoint]
|
|
795
926
|
);
|
|
796
927
|
const userRef = (0, import_react3.useRef)(null);
|
|
797
928
|
const metadataRef = (0, import_react3.useRef)({});
|
|
@@ -842,6 +973,33 @@ function MushiProvider({ children, ...config }) {
|
|
|
842
973
|
setScreen(screen) {
|
|
843
974
|
screenRef.current = screen;
|
|
844
975
|
},
|
|
976
|
+
// Reporter API — scoped to the device's persistent reporter token
|
|
977
|
+
async listMyReports() {
|
|
978
|
+
const client = apiClientRef.current;
|
|
979
|
+
if (!client) return [];
|
|
980
|
+
await reporterTokenReadyRef.current;
|
|
981
|
+
const res = await client.listReporterReports(reporterTokenRef.current);
|
|
982
|
+
return res.ok ? res.data?.reports ?? [] : [];
|
|
983
|
+
},
|
|
984
|
+
async listMyComments(reportId) {
|
|
985
|
+
const client = apiClientRef.current;
|
|
986
|
+
if (!client) return [];
|
|
987
|
+
const res = await client.listReporterComments(reportId, reporterTokenRef.current);
|
|
988
|
+
return res.ok ? res.data.comments ?? [] : [];
|
|
989
|
+
},
|
|
990
|
+
async replyToReport(reportId, body) {
|
|
991
|
+
const client = apiClientRef.current;
|
|
992
|
+
if (!client) return null;
|
|
993
|
+
const res = await client.replyToReporterReport(reportId, reporterTokenRef.current, body);
|
|
994
|
+
return res.ok ? res.data.comment : null;
|
|
995
|
+
},
|
|
996
|
+
// Leaderboard — SDK-public anonymized hall-of-fame
|
|
997
|
+
async getHallOfFame(limit = 10) {
|
|
998
|
+
const client = apiClientRef.current;
|
|
999
|
+
if (!client) return [];
|
|
1000
|
+
const res = await client.getHallOfFame(limit);
|
|
1001
|
+
return res.ok ? res.data.data ?? [] : [];
|
|
1002
|
+
},
|
|
845
1003
|
// Rewards (P1)
|
|
846
1004
|
recordActivity(action, metadata) {
|
|
847
1005
|
if (!config.rewards?.enabled) return;
|
|
@@ -883,7 +1041,15 @@ function MushiProvider({ children, ...config }) {
|
|
|
883
1041
|
inset: config.widget?.inset
|
|
884
1042
|
}
|
|
885
1043
|
),
|
|
886
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1044
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1045
|
+
MushiBottomSheet,
|
|
1046
|
+
{
|
|
1047
|
+
visible: sheetVisible,
|
|
1048
|
+
onClose: close,
|
|
1049
|
+
screenshotDataUrl: sheetScreenshot ?? void 0,
|
|
1050
|
+
onClearScreenshot: () => setSheetScreenshot(null)
|
|
1051
|
+
}
|
|
1052
|
+
)
|
|
887
1053
|
] });
|
|
888
1054
|
}
|
|
889
1055
|
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,14 @@ 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
|
+
/** Fetch the project's top contributors by points (max 50). */
|
|
111
|
+
getHallOfFame(limit?: number): Promise<MushiHallOfFameEntry[]>;
|
|
100
112
|
/** Manually record a host-defined activity event. */
|
|
101
113
|
recordActivity(action: string, metadata?: Record<string, unknown>): void;
|
|
102
114
|
/** Returns the current user's tier. */
|
|
@@ -153,6 +165,10 @@ declare function useMushiWidget(): {
|
|
|
153
165
|
interface MushiBottomSheetProps {
|
|
154
166
|
visible: boolean;
|
|
155
167
|
onClose: () => void;
|
|
168
|
+
/** Base64 data-URI of the screenshot captured before the sheet opened. Optional. */
|
|
169
|
+
screenshotDataUrl?: string;
|
|
170
|
+
/** Called when the user removes the attached screenshot. */
|
|
171
|
+
onClearScreenshot?: () => void;
|
|
156
172
|
}
|
|
157
173
|
declare const MushiBottomSheet: FC<MushiBottomSheetProps>;
|
|
158
174
|
|
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,14 @@ 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
|
+
/** Fetch the project's top contributors by points (max 50). */
|
|
111
|
+
getHallOfFame(limit?: number): Promise<MushiHallOfFameEntry[]>;
|
|
100
112
|
/** Manually record a host-defined activity event. */
|
|
101
113
|
recordActivity(action: string, metadata?: Record<string, unknown>): void;
|
|
102
114
|
/** Returns the current user's tier. */
|
|
@@ -153,6 +165,10 @@ declare function useMushiWidget(): {
|
|
|
153
165
|
interface MushiBottomSheetProps {
|
|
154
166
|
visible: boolean;
|
|
155
167
|
onClose: () => void;
|
|
168
|
+
/** Base64 data-URI of the screenshot captured before the sheet opened. Optional. */
|
|
169
|
+
screenshotDataUrl?: string;
|
|
170
|
+
/** Called when the user removes the attached screenshot. */
|
|
171
|
+
onClearScreenshot?: () => void;
|
|
156
172
|
}
|
|
157
173
|
declare const MushiBottomSheet: FC<MushiBottomSheetProps>;
|
|
158
174
|
|
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,
|
|
@@ -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 = ({
|
|
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,10 @@ 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 [screenshotAttached, setScreenshotAttached] = useState(true);
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
if (visible) setScreenshotAttached(true);
|
|
330
|
+
}, [visible]);
|
|
320
331
|
const resetForm = useCallback(() => {
|
|
321
332
|
setCategory(null);
|
|
322
333
|
setDescription("");
|
|
@@ -388,13 +399,18 @@ var MushiBottomSheet = ({ visible, onClose }) => {
|
|
|
388
399
|
if (!category || !description.trim() || !mushi) return;
|
|
389
400
|
setPhase("sending");
|
|
390
401
|
try {
|
|
391
|
-
await mushi.submitReport({
|
|
402
|
+
await mushi.submitReport({
|
|
403
|
+
category,
|
|
404
|
+
description: description.trim(),
|
|
405
|
+
screenshotDataUrl: screenshotDataUrl && screenshotAttached ? screenshotDataUrl : void 0
|
|
406
|
+
});
|
|
392
407
|
setPhase("sent");
|
|
393
408
|
setTimeout(handleClose, 1400);
|
|
394
409
|
} catch {
|
|
395
410
|
setPhase("form");
|
|
396
411
|
}
|
|
397
412
|
};
|
|
413
|
+
const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
|
|
398
414
|
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
415
|
const canSubmit = !!category && description.trim().length > 0 && phase === "form";
|
|
400
416
|
return /* @__PURE__ */ jsx(Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ jsxs(
|
|
@@ -475,6 +491,32 @@ var MushiBottomSheet = ({ visible, onClose }) => {
|
|
|
475
491
|
editable: phase === "form"
|
|
476
492
|
}
|
|
477
493
|
),
|
|
494
|
+
activeScreenshot ? /* @__PURE__ */ jsxs(View, { style: s.screenshotRow, children: [
|
|
495
|
+
/* @__PURE__ */ jsx(
|
|
496
|
+
Image,
|
|
497
|
+
{
|
|
498
|
+
source: { uri: activeScreenshot },
|
|
499
|
+
style: s.screenshotThumb,
|
|
500
|
+
accessibilityLabel: "Attached screenshot"
|
|
501
|
+
}
|
|
502
|
+
),
|
|
503
|
+
/* @__PURE__ */ jsxs(View, { style: s.screenshotMeta, children: [
|
|
504
|
+
/* @__PURE__ */ jsx(Text, { style: [s.screenshotLabel, { color: colors.text }], children: "Screenshot attached" }),
|
|
505
|
+
/* @__PURE__ */ jsx(Text, { style: [s.screenshotSub, { color: colors.sub }], children: "Helps the team see the issue" })
|
|
506
|
+
] }),
|
|
507
|
+
/* @__PURE__ */ jsx(
|
|
508
|
+
TouchableOpacity,
|
|
509
|
+
{
|
|
510
|
+
onPress: () => {
|
|
511
|
+
setScreenshotAttached(false);
|
|
512
|
+
onClearScreenshot?.();
|
|
513
|
+
},
|
|
514
|
+
hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
|
|
515
|
+
accessibilityLabel: "Remove screenshot",
|
|
516
|
+
children: /* @__PURE__ */ jsx(Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
] }) : null,
|
|
478
520
|
/* @__PURE__ */ jsx(
|
|
479
521
|
TouchableOpacity,
|
|
480
522
|
{
|
|
@@ -587,6 +629,39 @@ var s = StyleSheet.create({
|
|
|
587
629
|
sentText: {
|
|
588
630
|
fontSize: 20,
|
|
589
631
|
fontWeight: "600"
|
|
632
|
+
},
|
|
633
|
+
screenshotRow: {
|
|
634
|
+
flexDirection: "row",
|
|
635
|
+
alignItems: "center",
|
|
636
|
+
gap: 10,
|
|
637
|
+
marginBottom: 12,
|
|
638
|
+
paddingHorizontal: 10,
|
|
639
|
+
paddingVertical: 8,
|
|
640
|
+
borderRadius: 10,
|
|
641
|
+
backgroundColor: "rgba(128,128,128,0.1)"
|
|
642
|
+
},
|
|
643
|
+
screenshotThumb: {
|
|
644
|
+
width: 48,
|
|
645
|
+
height: 36,
|
|
646
|
+
borderRadius: 4,
|
|
647
|
+
backgroundColor: "#000",
|
|
648
|
+
flexShrink: 0
|
|
649
|
+
},
|
|
650
|
+
screenshotMeta: {
|
|
651
|
+
flex: 1,
|
|
652
|
+
gap: 2
|
|
653
|
+
},
|
|
654
|
+
screenshotLabel: {
|
|
655
|
+
fontSize: 13,
|
|
656
|
+
fontWeight: "600"
|
|
657
|
+
},
|
|
658
|
+
screenshotSub: {
|
|
659
|
+
fontSize: 11
|
|
660
|
+
},
|
|
661
|
+
screenshotRemove: {
|
|
662
|
+
fontSize: 16,
|
|
663
|
+
fontWeight: "700",
|
|
664
|
+
paddingHorizontal: 4
|
|
590
665
|
}
|
|
591
666
|
});
|
|
592
667
|
|
|
@@ -678,6 +753,15 @@ function MushiProvider({ children, ...config }) {
|
|
|
678
753
|
const queueRef = useRef3(null);
|
|
679
754
|
const apiClientRef = useRef3(null);
|
|
680
755
|
const [sheetVisible, setSheetVisible] = useState2(false);
|
|
756
|
+
const [sheetScreenshot, setSheetScreenshot] = useState2(null);
|
|
757
|
+
const reporterTokenRef = useRef3(`rn-${config.projectId}-anon`);
|
|
758
|
+
let resolveReporterTokenReady = () => {
|
|
759
|
+
};
|
|
760
|
+
const reporterTokenReadyRef = useRef3(
|
|
761
|
+
new Promise((resolve) => {
|
|
762
|
+
resolveReporterTokenReady = resolve;
|
|
763
|
+
})
|
|
764
|
+
);
|
|
681
765
|
if (config.endpoint !== void 0 && config.endpoint.trim() === "") {
|
|
682
766
|
throw new Error(
|
|
683
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."
|
|
@@ -703,6 +787,27 @@ function MushiProvider({ children, ...config }) {
|
|
|
703
787
|
});
|
|
704
788
|
queueRef.current.flush().catch(() => {
|
|
705
789
|
});
|
|
790
|
+
(async () => {
|
|
791
|
+
const TOKEN_KEY = "@mushi:reporter_token";
|
|
792
|
+
try {
|
|
793
|
+
const AsyncStorage = (await import("@react-native-async-storage/async-storage")).default;
|
|
794
|
+
const existing = await AsyncStorage.getItem(TOKEN_KEY);
|
|
795
|
+
if (existing && existing.startsWith("mushi_")) {
|
|
796
|
+
reporterTokenRef.current = existing;
|
|
797
|
+
} else {
|
|
798
|
+
const fresh = `mushi_${newUuid()}`;
|
|
799
|
+
reporterTokenRef.current = fresh;
|
|
800
|
+
await AsyncStorage.setItem(TOKEN_KEY, fresh);
|
|
801
|
+
}
|
|
802
|
+
resolveReporterTokenReady();
|
|
803
|
+
} catch {
|
|
804
|
+
if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
|
|
805
|
+
reporterTokenRef.current = `mushi_${newUuid()}`;
|
|
806
|
+
}
|
|
807
|
+
resolveReporterTokenReady();
|
|
808
|
+
}
|
|
809
|
+
})().catch(() => {
|
|
810
|
+
});
|
|
706
811
|
return () => {
|
|
707
812
|
consoleRef.current?.restore();
|
|
708
813
|
networkRef.current?.restore();
|
|
@@ -722,7 +827,25 @@ function MushiProvider({ children, ...config }) {
|
|
|
722
827
|
}
|
|
723
828
|
return () => unsubscribe?.();
|
|
724
829
|
}, []);
|
|
725
|
-
const open = useCallback3(() =>
|
|
830
|
+
const open = useCallback3(() => {
|
|
831
|
+
let vshot = null;
|
|
832
|
+
try {
|
|
833
|
+
vshot = __require("react-native-view-shot");
|
|
834
|
+
} catch {
|
|
835
|
+
}
|
|
836
|
+
if (!vshot) {
|
|
837
|
+
setSheetScreenshot(null);
|
|
838
|
+
setSheetVisible(true);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
vshot.captureScreen({ format: "jpg", quality: 0.7, result: "data-uri" }).then((url) => {
|
|
842
|
+
setSheetScreenshot(url);
|
|
843
|
+
}).catch(() => {
|
|
844
|
+
setSheetScreenshot(null);
|
|
845
|
+
}).finally(() => {
|
|
846
|
+
setSheetVisible(true);
|
|
847
|
+
});
|
|
848
|
+
}, []);
|
|
726
849
|
const close = useCallback3(() => setSheetVisible(false), []);
|
|
727
850
|
const attachTo = useCallback3(() => ({ onPress: open }), [open]);
|
|
728
851
|
useEffect2(() => {
|
|
@@ -756,8 +879,16 @@ function MushiProvider({ children, ...config }) {
|
|
|
756
879
|
const submitReport = useCallback3(
|
|
757
880
|
async (data) => {
|
|
758
881
|
const deviceInfo = getDeviceInfo();
|
|
882
|
+
const contextMeta = {};
|
|
883
|
+
if (userRef.current?.id) contextMeta.userId = userRef.current.id;
|
|
884
|
+
if (userRef.current?.email) contextMeta.userEmail = userRef.current.email;
|
|
885
|
+
if (userRef.current?.name) contextMeta.userName = userRef.current.name;
|
|
886
|
+
if (screenRef.current?.name) contextMeta.screenName = screenRef.current.name;
|
|
887
|
+
if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
|
|
888
|
+
if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
|
|
889
|
+
const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
|
|
759
890
|
const report = {
|
|
760
|
-
id:
|
|
891
|
+
id: newUuid(),
|
|
761
892
|
projectId: config.projectId,
|
|
762
893
|
category: data.category,
|
|
763
894
|
description: data.description,
|
|
@@ -766,7 +897,7 @@ function MushiProvider({ children, ...config }) {
|
|
|
766
897
|
platform: deviceInfo.platform ?? "mobile",
|
|
767
898
|
language: deviceInfo.locale ?? "en",
|
|
768
899
|
viewport: { width: deviceInfo.screenWidth ?? 0, height: deviceInfo.screenHeight ?? 0 },
|
|
769
|
-
url: "",
|
|
900
|
+
url: screenRef.current?.route ?? "",
|
|
770
901
|
referrer: "",
|
|
771
902
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
772
903
|
timezone: deviceInfo.timezone ?? "UTC"
|
|
@@ -776,7 +907,9 @@ function MushiProvider({ children, ...config }) {
|
|
|
776
907
|
...entry,
|
|
777
908
|
status: entry.status ?? 0
|
|
778
909
|
})) ?? [],
|
|
779
|
-
|
|
910
|
+
screenshotDataUrl: data.screenshotDataUrl,
|
|
911
|
+
metadata: merged,
|
|
912
|
+
reporterToken: reporterTokenRef.current,
|
|
780
913
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
781
914
|
};
|
|
782
915
|
const client = apiClientRef.current;
|
|
@@ -786,7 +919,7 @@ function MushiProvider({ children, ...config }) {
|
|
|
786
919
|
await queueRef.current?.enqueue(report);
|
|
787
920
|
}
|
|
788
921
|
},
|
|
789
|
-
[config.projectId,
|
|
922
|
+
[config.projectId, apiEndpoint]
|
|
790
923
|
);
|
|
791
924
|
const userRef = useRef3(null);
|
|
792
925
|
const metadataRef = useRef3({});
|
|
@@ -837,6 +970,33 @@ function MushiProvider({ children, ...config }) {
|
|
|
837
970
|
setScreen(screen) {
|
|
838
971
|
screenRef.current = screen;
|
|
839
972
|
},
|
|
973
|
+
// Reporter API — scoped to the device's persistent reporter token
|
|
974
|
+
async listMyReports() {
|
|
975
|
+
const client = apiClientRef.current;
|
|
976
|
+
if (!client) return [];
|
|
977
|
+
await reporterTokenReadyRef.current;
|
|
978
|
+
const res = await client.listReporterReports(reporterTokenRef.current);
|
|
979
|
+
return res.ok ? res.data?.reports ?? [] : [];
|
|
980
|
+
},
|
|
981
|
+
async listMyComments(reportId) {
|
|
982
|
+
const client = apiClientRef.current;
|
|
983
|
+
if (!client) return [];
|
|
984
|
+
const res = await client.listReporterComments(reportId, reporterTokenRef.current);
|
|
985
|
+
return res.ok ? res.data.comments ?? [] : [];
|
|
986
|
+
},
|
|
987
|
+
async replyToReport(reportId, body) {
|
|
988
|
+
const client = apiClientRef.current;
|
|
989
|
+
if (!client) return null;
|
|
990
|
+
const res = await client.replyToReporterReport(reportId, reporterTokenRef.current, body);
|
|
991
|
+
return res.ok ? res.data.comment : null;
|
|
992
|
+
},
|
|
993
|
+
// Leaderboard — SDK-public anonymized hall-of-fame
|
|
994
|
+
async getHallOfFame(limit = 10) {
|
|
995
|
+
const client = apiClientRef.current;
|
|
996
|
+
if (!client) return [];
|
|
997
|
+
const res = await client.getHallOfFame(limit);
|
|
998
|
+
return res.ok ? res.data.data ?? [] : [];
|
|
999
|
+
},
|
|
840
1000
|
// Rewards (P1)
|
|
841
1001
|
recordActivity(action, metadata) {
|
|
842
1002
|
if (!config.rewards?.enabled) return;
|
|
@@ -878,7 +1038,15 @@ function MushiProvider({ children, ...config }) {
|
|
|
878
1038
|
inset: config.widget?.inset
|
|
879
1039
|
}
|
|
880
1040
|
),
|
|
881
|
-
/* @__PURE__ */ jsx3(
|
|
1041
|
+
/* @__PURE__ */ jsx3(
|
|
1042
|
+
MushiBottomSheet,
|
|
1043
|
+
{
|
|
1044
|
+
visible: sheetVisible,
|
|
1045
|
+
onClose: close,
|
|
1046
|
+
screenshotDataUrl: sheetScreenshot ?? void 0,
|
|
1047
|
+
onClearScreenshot: () => setSheetScreenshot(null)
|
|
1048
|
+
}
|
|
1049
|
+
)
|
|
882
1050
|
] });
|
|
883
1051
|
}
|
|
884
1052
|
function useMushiContext() {
|
package/package.json
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushi-mushi/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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": "
|
|
36
|
+
"@mushi-mushi/core": "^1.10.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
|
+
}
|