@mushi-mushi/react-native 0.13.0 → 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/CONTRIBUTING.md CHANGED
@@ -101,17 +101,6 @@ Releases are fully automated. Maintainers don't run `npm publish` by hand.
101
101
 
102
102
  If GitHub's anti-loop protection suppresses the auto re-fire (the squash merge can be attributed to `github-actions[bot]`), trigger the workflow manually: **Actions → release → Run workflow → master**.
103
103
 
104
- ### Known CI/CD quirks and their automatic safeguards
105
-
106
- A handful of GitHub-Actions × Changesets edge cases have caused release-pipeline stalls in the past. Each is now caught automatically — keep these in mind when you see the symptom:
107
-
108
- | Symptom | Root cause | Automatic safeguard |
109
- | --- | --- | --- |
110
- | The `Build & Test` required check never registers on the `chore: version packages` PR — the PR stays "Required check missing" forever | `changeset-release/master` is pushed by `github-actions[bot]`. GitHub silently drops the downstream `pull_request` event to prevent bot loops (observed on PR #45, #102, #124). | `ci.yml` now also triggers on `push` to `changeset-release/master`, so `Build & Test` reports against the head commit directly. No empty-commit nudge needed. |
111
- | Release workflow fails with `No commits between master and changeset-release/master` after merging a PR with a new changeset. | A `.changeset/*.md` file whose YAML frontmatter only targets packages listed in `.changeset/config.json#ignore` (e.g. `@mushi-mushi/server`, `@mushi-mushi/admin`). `changeset version` produces no bumps, the version PR is empty, the next push errors (PR #102 / #121, 2026-05-19). | `pnpm check:changeset-orphans` runs in both `ci.yml` and `release.yml`. PR CI fails with an actionable message naming the orphan file *before* it can reach master. If you legitimately need to record an internal-only change, omit the changeset entirely — the diff lives in git history. |
112
- | Release workflow's `Audit signatures of installed dependencies` step fails with `npm ETARGET / No matching version found for @mushi-mushi/<pkg>@<ver>` seconds after the publish step succeeded. | npm's registry CDN can take up to ~30s to propagate a freshly-published manifest. The audit step shells out to `npm install` against the just-published version and races the CDN (observed 2026-05-20, run 26149167393). | The audit step retries with exponential backoff (1, 2, 4, 8, 16, 32s — 63s total) before failing. Sigstore signatures are written at publish time, so a one-off audit failure never indicates a corrupted package — `pnpm view <pkg> version` is the ground truth. |
113
- | Push to `master` after merging a PR doesn't fire the `Release` workflow. | Same anti-loop protection: when a squash merge is attributed to `github-actions[bot]`, GitHub may suppress the downstream `push` trigger. Sporadic — observed twice in the last 60 days. | `release.yml` keeps `workflow_dispatch` as the manual fallback. Recovery: **Actions → Release → Run workflow → master**. The `Build & Verify` job re-runs identically to the auto-fired path. |
114
-
115
104
  ### Adding a brand-new publishable package
116
105
 
117
106
  Trusted Publisher bindings are configured **per package** on `npmjs.com` and require the package to already exist on the registry. New packages therefore need a one-time bootstrap before OIDC can take over.
package/README.md CHANGED
@@ -115,3 +115,9 @@ See the full [CHANGELOG](https://github.com/kensaurus/mushi-mushi/blob/master/pa
115
115
  ## License
116
116
 
117
117
  MIT
118
+
119
+
120
+ <!-- mushi-readme-stats-footer -->
121
+ ---
122
+
123
+ <sub>Monorepo scale (June 2026): 43 edge functions · 234 SQL migrations · 13 outbound plugins · 11 inbound adapters. Canonical counts: <a href="https://github.com/kensaurus/mushi-mushi/blob/master/docs/stats.md">docs/stats.md</a> · <code>pnpm docs-stats</code></sub>
package/dist/index.cjs CHANGED
@@ -319,7 +319,12 @@ var CATEGORIES = [
319
319
  { key: "confusing", emoji: "\u{1F615}", label: "Confusing" },
320
320
  { key: "other", emoji: "\u{1F4AC}", label: "Other" }
321
321
  ];
322
- var MushiBottomSheet = ({ visible, onClose }) => {
322
+ var MushiBottomSheet = ({
323
+ visible,
324
+ onClose,
325
+ screenshotDataUrl,
326
+ onClearScreenshot
327
+ }) => {
323
328
  const mushi = useMushiContext();
324
329
  const scheme = (0, import_react_native2.useColorScheme)();
325
330
  const dark = scheme === "dark";
@@ -328,6 +333,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({ category, description: description.trim() });
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)(() => setSheetVisible(true), []);
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: `rn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
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
- reporterToken: `rn-${config.projectId}-anon`,
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, config.apiKey, apiEndpoint]
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)(MushiBottomSheet, { visible: sheetVisible, onClose: close })
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 = ({ visible, onClose }) => {
313
+ var MushiBottomSheet = ({
314
+ visible,
315
+ onClose,
316
+ screenshotDataUrl,
317
+ onClearScreenshot
318
+ }) => {
312
319
  const mushi = useMushiContext();
313
320
  const scheme = useColorScheme();
314
321
  const dark = scheme === "dark";
@@ -317,6 +324,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({ category, description: description.trim() });
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(() => setSheetVisible(true), []);
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: `rn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
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
- reporterToken: `rn-${config.projectId}-anon`,
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, config.apiKey, apiEndpoint]
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(MushiBottomSheet, { visible: sheetVisible, onClose: close })
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@mushi-mushi/react-native",
3
- "version": "0.13.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",
@@ -12,7 +12,8 @@
12
12
  "@react-navigation/native": ">=6",
13
13
  "expo-sensors": ">=13",
14
14
  "react": ">=18",
15
- "react-native": ">=0.72"
15
+ "react-native": ">=0.72",
16
+ "react-native-view-shot": ">=3.8"
16
17
  },
17
18
  "peerDependenciesMeta": {
18
19
  "@react-navigation/native": {
@@ -26,10 +27,13 @@
26
27
  },
27
28
  "@react-native-community/netinfo": {
28
29
  "optional": true
30
+ },
31
+ "react-native-view-shot": {
32
+ "optional": true
29
33
  }
30
34
  },
31
35
  "dependencies": {
32
- "@mushi-mushi/core": "^1.5.0"
36
+ "@mushi-mushi/core": "^1.10.0"
33
37
  },
34
38
  "devDependencies": {
35
39
  "@types/react": "^19.2.14",