@mushi-mushi/react-native 0.16.0 → 0.17.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/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
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, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
4
+ import { MushiRewardsConfig, MushiPageContext, MushiBreadcrumb, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
5
5
  export { MushiHallOfFameEntry, MushiReporterComment, MushiReporterReport } from '@mushi-mushi/core';
6
6
 
7
7
  interface ConsoleEntry {
@@ -72,8 +72,19 @@ interface MushiRNConfig {
72
72
  console?: boolean;
73
73
  network?: boolean;
74
74
  navigation?: boolean;
75
+ /**
76
+ * Capture a screenshot of the current screen when the widget opens.
77
+ * Defaults to `true`. Requires the optional `react-native-view-shot`
78
+ * peer dependency — when it isn't installed, capture is skipped silently
79
+ * (the report is still sent without an image). The user always sees a
80
+ * thumbnail in the sheet and can remove it before submitting, so
81
+ * sensitive screens (balances, PII) are never sent without consent.
82
+ */
83
+ screenshot?: boolean;
75
84
  maxConsoleEntries?: number;
76
85
  maxNetworkEntries?: number;
86
+ /** Hard cap on the breadcrumb / repro-timeline ring buffer. Default 50. */
87
+ maxBreadcrumbs?: number;
77
88
  };
78
89
  storage?: {
79
90
  maxQueueSize?: number;
@@ -104,6 +115,27 @@ interface MushiRNInstance {
104
115
  provider?: string;
105
116
  [k: string]: unknown;
106
117
  }): void;
118
+ /**
119
+ * Alias for {@link identify} matching the web SDK's `Mushi.setUser()`.
120
+ * Accepts an object so hosts can call `setUser({ id, email, name })`.
121
+ */
122
+ setUser(user: {
123
+ id: string;
124
+ email?: string;
125
+ name?: string;
126
+ provider?: string;
127
+ }): void;
128
+ /**
129
+ * Identify the current end user with a signed Mushi identity JWT minted by
130
+ * the host server. Mirrors the web SDK; the backend verifies the signature
131
+ * before trusting any claim. Pass `null` on logout to go anonymous.
132
+ */
133
+ identifyWithToken(token: string | null): void;
134
+ /**
135
+ * Publish the current screen's context so the assistant can answer
136
+ * page-aware questions. Pass `null` to clear.
137
+ */
138
+ publishPageContext(context: MushiPageContext | null): void;
107
139
  /** Attach arbitrary key/value metadata to subsequent reports. */
108
140
  setMetadata(key: string, value: unknown): void;
109
141
  /** Set the current screen context attached to subsequent reports. */
@@ -112,6 +144,10 @@ interface MushiRNInstance {
112
144
  route?: string;
113
145
  feature?: string;
114
146
  }): void;
147
+ /** Append a breadcrumb to the repro timeline ring buffer. */
148
+ addBreadcrumb(crumb: Omit<MushiBreadcrumb, 'timestamp'> & {
149
+ timestamp?: number;
150
+ }): void;
115
151
  /** List this device's reports ordered by most recent. Returns [] on failure. */
116
152
  listMyReports(): Promise<MushiReporterReport[]>;
117
153
  /** List admin + reporter comments on a specific report. */
@@ -131,8 +167,9 @@ interface MushiRNInstance {
131
167
  /** Returns the current user's reputation + point totals. */
132
168
  getReputation(): Promise<MushiReputationResult | null>;
133
169
  }
134
- declare function MushiProvider({ children, ...config }: MushiRNConfig & {
170
+ declare function MushiProvider({ children, config: configProp, ...barePropConfig }: MushiRNConfig & {
135
171
  children: ReactNode;
172
+ config?: Partial<MushiRNConfig>;
136
173
  }): react_jsx_runtime.JSX.Element;
137
174
 
138
175
  declare function useMushi(): MushiRNInstance | null;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
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, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
4
+ import { MushiRewardsConfig, MushiPageContext, MushiBreadcrumb, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
5
5
  export { MushiHallOfFameEntry, MushiReporterComment, MushiReporterReport } from '@mushi-mushi/core';
6
6
 
7
7
  interface ConsoleEntry {
@@ -72,8 +72,19 @@ interface MushiRNConfig {
72
72
  console?: boolean;
73
73
  network?: boolean;
74
74
  navigation?: boolean;
75
+ /**
76
+ * Capture a screenshot of the current screen when the widget opens.
77
+ * Defaults to `true`. Requires the optional `react-native-view-shot`
78
+ * peer dependency — when it isn't installed, capture is skipped silently
79
+ * (the report is still sent without an image). The user always sees a
80
+ * thumbnail in the sheet and can remove it before submitting, so
81
+ * sensitive screens (balances, PII) are never sent without consent.
82
+ */
83
+ screenshot?: boolean;
75
84
  maxConsoleEntries?: number;
76
85
  maxNetworkEntries?: number;
86
+ /** Hard cap on the breadcrumb / repro-timeline ring buffer. Default 50. */
87
+ maxBreadcrumbs?: number;
77
88
  };
78
89
  storage?: {
79
90
  maxQueueSize?: number;
@@ -104,6 +115,27 @@ interface MushiRNInstance {
104
115
  provider?: string;
105
116
  [k: string]: unknown;
106
117
  }): void;
118
+ /**
119
+ * Alias for {@link identify} matching the web SDK's `Mushi.setUser()`.
120
+ * Accepts an object so hosts can call `setUser({ id, email, name })`.
121
+ */
122
+ setUser(user: {
123
+ id: string;
124
+ email?: string;
125
+ name?: string;
126
+ provider?: string;
127
+ }): void;
128
+ /**
129
+ * Identify the current end user with a signed Mushi identity JWT minted by
130
+ * the host server. Mirrors the web SDK; the backend verifies the signature
131
+ * before trusting any claim. Pass `null` on logout to go anonymous.
132
+ */
133
+ identifyWithToken(token: string | null): void;
134
+ /**
135
+ * Publish the current screen's context so the assistant can answer
136
+ * page-aware questions. Pass `null` to clear.
137
+ */
138
+ publishPageContext(context: MushiPageContext | null): void;
107
139
  /** Attach arbitrary key/value metadata to subsequent reports. */
108
140
  setMetadata(key: string, value: unknown): void;
109
141
  /** Set the current screen context attached to subsequent reports. */
@@ -112,6 +144,10 @@ interface MushiRNInstance {
112
144
  route?: string;
113
145
  feature?: string;
114
146
  }): void;
147
+ /** Append a breadcrumb to the repro timeline ring buffer. */
148
+ addBreadcrumb(crumb: Omit<MushiBreadcrumb, 'timestamp'> & {
149
+ timestamp?: number;
150
+ }): void;
115
151
  /** List this device's reports ordered by most recent. Returns [] on failure. */
116
152
  listMyReports(): Promise<MushiReporterReport[]>;
117
153
  /** List admin + reporter comments on a specific report. */
@@ -131,8 +167,9 @@ interface MushiRNInstance {
131
167
  /** Returns the current user's reputation + point totals. */
132
168
  getReputation(): Promise<MushiReputationResult | null>;
133
169
  }
134
- declare function MushiProvider({ children, ...config }: MushiRNConfig & {
170
+ declare function MushiProvider({ children, config: configProp, ...barePropConfig }: MushiRNConfig & {
135
171
  children: ReactNode;
172
+ config?: Partial<MushiRNConfig>;
136
173
  }): react_jsx_runtime.JSX.Element;
137
174
 
138
175
  declare function useMushi(): MushiRNInstance | null;
package/dist/index.js CHANGED
@@ -17,8 +17,10 @@ import {
17
17
  } from "react";
18
18
  import {
19
19
  createApiClient,
20
+ createBreadcrumbBuffer,
20
21
  DEFAULT_API_ENDPOINT,
21
- newUuid
22
+ newUuid,
23
+ parseIdentityToken
22
24
  } from "@mushi-mushi/core";
23
25
 
24
26
  // src/capture/console-capture.ts
@@ -136,6 +138,30 @@ function getDeviceInfo() {
136
138
  };
137
139
  }
138
140
 
141
+ // src/capture/fingerprint.ts
142
+ function nonCryptoHashHex(input) {
143
+ let hash = 0;
144
+ for (let i = 0; i < input.length; i++) {
145
+ hash = (hash << 5) - hash + input.charCodeAt(i);
146
+ hash |= 0;
147
+ }
148
+ return `rnfp_${(hash >>> 0).toString(16).padStart(8, "0")}`;
149
+ }
150
+ function getDeviceFingerprintHash(device) {
151
+ const inputs = {
152
+ platform: device.platform,
153
+ osVersion: String(device.osVersion ?? ""),
154
+ screenWidth: device.screenWidth,
155
+ screenHeight: device.screenHeight,
156
+ pixelRatio: device.pixelRatio,
157
+ locale: device.locale ?? "",
158
+ timezone: device.timezone ?? ""
159
+ };
160
+ const serialised = JSON.stringify(inputs);
161
+ if (!device.platform && !device.screenWidth) return null;
162
+ return nonCryptoHashHex(serialised);
163
+ }
164
+
139
165
  // src/storage/async-storage-queue.ts
140
166
  var QUEUE_KEY = "@mushi:offline_queue";
141
167
  var AsyncStorageQueue = class {
@@ -309,10 +335,17 @@ import {
309
335
  ScrollView,
310
336
  useColorScheme
311
337
  } from "react-native";
338
+ import { mushiPalette } from "@mushi-mushi/core";
312
339
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
313
340
  var { height: SCREEN_HEIGHT } = Dimensions2.get("window");
314
341
  var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
315
342
  var DISMISS_THRESHOLD = 80;
343
+ var NEON = {
344
+ accent: "#0FFF50",
345
+ accentHover: "#00E646",
346
+ ink: "#0a1a0a",
347
+ border: "#00C43A"
348
+ };
316
349
  var CATEGORIES = [
317
350
  { key: "bug", emoji: "\u{1F41B}", label: "Bug" },
318
351
  { key: "slow", emoji: "\u{1F422}", label: "Slow" },
@@ -459,7 +492,8 @@ var MushiBottomSheet = ({
459
492
  }
460
493
  };
461
494
  const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
462
- const colors = dark ? { bg: "#1c1c1e", text: "#f2f2f7", sub: "#8e8e93", card: "#2c2c2e", accent: "#0a84ff", border: "#38383a", backdrop: "rgba(0,0,0,0.6)", disabled: "#3a3a3c", disabledText: "#636366" } : { bg: "#ffffff", text: "#1c1c1e", sub: "#8e8e93", card: "#f2f2f7", accent: "#007aff", border: "#e5e5ea", backdrop: "rgba(0,0,0,0.35)", disabled: "#d1d1d6", disabledText: "#8e8e93" };
495
+ const pal = mushiPalette(dark ? "dark" : "light");
496
+ const colors = dark ? { bg: pal.paper, text: pal.ink, sub: pal.inkMuted, card: pal.paperRaised, accent: NEON.accent, accentInk: NEON.ink, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.6)", disabled: "#3a3a3c", disabledText: pal.inkFaint } : { bg: pal.paperRaised, text: pal.ink, sub: pal.inkMuted, card: pal.paper, accent: NEON.accent, accentInk: NEON.ink, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.35)", disabled: "#d1d1d6", disabledText: pal.inkFaint };
463
497
  const canSubmit = !!category && description.trim().length > 0 && phase === "form";
464
498
  return /* @__PURE__ */ jsx(Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ jsxs(
465
499
  KeyboardAvoidingView,
@@ -484,6 +518,10 @@ var MushiBottomSheet = ({
484
518
  ],
485
519
  children: [
486
520
  /* @__PURE__ */ jsx(View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ jsx(View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
521
+ /* @__PURE__ */ jsxs(View, { style: [s.brandHeader, { backgroundColor: NEON.accent, borderBottomColor: NEON.border }], children: [
522
+ /* @__PURE__ */ jsx(Text, { style: [s.brandEyebrow, { color: NEON.ink }], children: "MUSHI \xB7 BETA" }),
523
+ /* @__PURE__ */ jsx(Text, { style: [s.brandTitle, { color: NEON.ink }], children: sheetTab === "inbox" ? "Your reports" : sheetTab === "community" ? "Community" : "Report an issue" })
524
+ ] }),
487
525
  /* @__PURE__ */ jsx(View, { style: s.tabRow, children: ["report", "inbox", "community"].map((tab) => /* @__PURE__ */ jsx(
488
526
  TouchableOpacity,
489
527
  {
@@ -493,7 +531,7 @@ var MushiBottomSheet = ({
493
531
  else setSelectedReportId(null);
494
532
  },
495
533
  style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
496
- children: /* @__PURE__ */ jsx(Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.accent : colors.sub }], children: tab === "report" ? "Report" : tab === "inbox" ? "Inbox" : "\u{1F310}" })
534
+ children: /* @__PURE__ */ jsx(Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.text : colors.sub }], children: tab === "report" ? "Report" : tab === "inbox" ? "Your reports" : "Community" })
497
535
  },
498
536
  tab
499
537
  )) }),
@@ -506,7 +544,7 @@ var MushiBottomSheet = ({
506
544
  VERIFIABLE_STATUSES.has(
507
545
  inboxReports.find((r) => r.id === selectedReportId)?.status ?? ""
508
546
  ) && /* @__PURE__ */ jsxs(View, { style: s.verifyRow, children: [
509
- /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ jsx(Text, { style: s.submitText, children: "Yes, fixed" }) }),
547
+ /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: "Yes, fixed" }) }),
510
548
  /* @__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" }) })
511
549
  ] }),
512
550
  /* @__PURE__ */ jsx(
@@ -519,12 +557,11 @@ var MushiBottomSheet = ({
519
557
  onChangeText: setReplyText
520
558
  }
521
559
  ),
522
- /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ jsx(Text, { style: s.submitText, children: "Send" }) })
560
+ /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: "Send" }) })
523
561
  ] }) : inboxReports.map((r) => /* @__PURE__ */ jsxs(TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
524
562
  /* @__PURE__ */ jsx(Text, { style: { color: colors.text, fontWeight: "600" }, numberOfLines: 1, children: (r.summary ?? r.description ?? "Report").slice(0, 60) }),
525
563
  /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: r.status })
526
564
  ] }, r.id)) }) : sheetTab === "community" ? /* @__PURE__ */ jsxs(ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: [
527
- /* @__PURE__ */ jsx(Text, { style: [s.title, { color: colors.text }], children: "\u{1F310} Mushi Community" }),
528
565
  /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 13, marginBottom: 16, lineHeight: 18 }, children: "Sign in to track your reports across apps and see the global leaderboard. No password needed." }),
529
566
  /* @__PURE__ */ jsx(
530
567
  TextInput,
@@ -538,7 +575,7 @@ var MushiBottomSheet = ({
538
575
  textContentType: "emailAddress"
539
576
  }
540
577
  ),
541
- /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], children: /* @__PURE__ */ jsx(Text, { style: s.submitText, children: "Send sign-in link \u2192" }) }),
578
+ /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: "Send sign-in link \u2192" }) }),
542
579
  /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11, textAlign: "center", marginTop: 12 }, children: "We'll email you a one-tap sign-in link. No password, ever." })
543
580
  ] }) : phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
544
581
  /* @__PURE__ */ jsx(Text, { style: [s.sentEmoji], children: "\u2705" }),
@@ -550,7 +587,7 @@ var MushiBottomSheet = ({
550
587
  keyboardShouldPersistTaps: "handled",
551
588
  contentContainerStyle: { paddingBottom: 24 },
552
589
  children: [
553
- /* @__PURE__ */ jsx(Text, { style: [s.title, { color: colors.text }], children: "Report an issue" }),
590
+ /* @__PURE__ */ jsx(Text, { style: [s.stepLabel, { color: colors.sub }], children: "What kind of issue?" }),
554
591
  /* @__PURE__ */ jsx(View, { style: s.catRow, children: CATEGORIES.map((c) => {
555
592
  const active = category === c.key;
556
593
  return /* @__PURE__ */ jsxs(
@@ -572,7 +609,7 @@ var MushiBottomSheet = ({
572
609
  {
573
610
  style: [
574
611
  s.catLabel,
575
- { color: active ? "#fff" : colors.text }
612
+ { color: active ? colors.accentInk : colors.text }
576
613
  ],
577
614
  children: c.label
578
615
  }
@@ -638,7 +675,7 @@ var MushiBottomSheet = ({
638
675
  s.submitBtn,
639
676
  { backgroundColor: canSubmit ? colors.accent : colors.disabled }
640
677
  ],
641
- children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, !canSubmit && { color: colors.disabledText }], children: phase === "sending" ? "Sending\u2026" : "Submit" })
678
+ children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: canSubmit ? colors.accentInk : colors.disabledText }], children: phase === "sending" ? "Sending\u2026" : "Submit report" })
642
679
  }
643
680
  )
644
681
  ]
@@ -681,6 +718,24 @@ var s = StyleSheet.create({
681
718
  borderRadius: 3,
682
719
  opacity: 0.5
683
720
  },
721
+ brandHeader: {
722
+ paddingHorizontal: 20,
723
+ paddingTop: 10,
724
+ paddingBottom: 12,
725
+ borderBottomWidth: 1.5
726
+ },
727
+ brandEyebrow: {
728
+ fontSize: 10,
729
+ fontWeight: "700",
730
+ letterSpacing: 1.2,
731
+ marginBottom: 2,
732
+ opacity: 0.7
733
+ },
734
+ brandTitle: {
735
+ fontSize: 18,
736
+ fontWeight: "800",
737
+ letterSpacing: -0.2
738
+ },
684
739
  body: {
685
740
  paddingHorizontal: 20,
686
741
  paddingTop: 4
@@ -690,6 +745,14 @@ var s = StyleSheet.create({
690
745
  fontWeight: "700",
691
746
  marginBottom: 16
692
747
  },
748
+ stepLabel: {
749
+ fontSize: 12,
750
+ fontWeight: "600",
751
+ letterSpacing: 0.2,
752
+ marginTop: 8,
753
+ marginBottom: 10,
754
+ textTransform: "uppercase"
755
+ },
693
756
  catRow: {
694
757
  flexDirection: "row",
695
758
  justifyContent: "space-between",
@@ -825,6 +888,7 @@ import {
825
888
  Dimensions as Dimensions3,
826
889
  useColorScheme as useColorScheme2
827
890
  } from "react-native";
891
+ import { mushiPalette as mushiPalette2, MUSHI_COPY } from "@mushi-mushi/core";
828
892
  import { jsx as jsx2 } from "react/jsx-runtime";
829
893
  var FAB_SIZE = 56;
830
894
  var EDGE_PADDING = 12;
@@ -876,7 +940,7 @@ var MushiFloatingButton = ({
876
940
  const currentX = useRef2(baseX);
877
941
  const currentY = useRef2(0);
878
942
  const isDragging = useRef2(false);
879
- const accentColor = accent ?? (dark ? "#0a84ff" : "#007aff");
943
+ const accentColor = accent ?? mushiPalette2(dark ? "dark" : "light").accent;
880
944
  useEffect2(() => {
881
945
  if (!persist) return;
882
946
  void loadStoredPos().then((pos) => {
@@ -986,7 +1050,7 @@ var MushiFloatingButton = ({
986
1050
  onStartShouldSetResponder: () => !draggable,
987
1051
  onResponderGrant: handlePressIn,
988
1052
  onResponderRelease: handlePressOut,
989
- children: /* @__PURE__ */ jsx2(Animated2.Text, { style: s2.emoji, children: "\u{1F41B}" })
1053
+ children: /* @__PURE__ */ jsx2(Animated2.Text, { style: s2.emoji, children: MUSHI_COPY.triggerText })
990
1054
  }
991
1055
  );
992
1056
  };
@@ -1009,14 +1073,53 @@ var s2 = StyleSheet2.create({
1009
1073
  }
1010
1074
  });
1011
1075
 
1076
+ // src/version.ts
1077
+ var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
1078
+ var MUSHI_SDK_VERSION = "0.17.0" ? "0.17.0" : "0.17.0";
1079
+
1012
1080
  // src/provider.tsx
1013
1081
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1014
1082
  var MushiContext = createContext(null);
1015
- function MushiProvider({ children, ...config }) {
1083
+ function breadcrumbToTimelineKind(category) {
1084
+ switch (category) {
1085
+ case "navigation":
1086
+ return "route";
1087
+ case "ui.click":
1088
+ case "ui.tap":
1089
+ return "click";
1090
+ case "xhr":
1091
+ case "fetch":
1092
+ case "network":
1093
+ return "request";
1094
+ default:
1095
+ return "log";
1096
+ }
1097
+ }
1098
+ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1099
+ const proc = globalThis.process;
1100
+ const envProjectId = proc?.env?.MUSHI_PROJECT_ID || proc?.env?.EXPO_PUBLIC_MUSHI_PROJECT_ID;
1101
+ const envApiKey = proc?.env?.MUSHI_API_KEY || proc?.env?.EXPO_PUBLIC_MUSHI_API_KEY;
1102
+ const config = {
1103
+ ...envProjectId ? { projectId: envProjectId } : {},
1104
+ ...envApiKey ? { apiKey: envApiKey } : {},
1105
+ ...configProp,
1106
+ ...barePropConfig
1107
+ };
1108
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
1109
+ if (!config.projectId || !config.apiKey) {
1110
+ console.warn(
1111
+ "[MushiProvider] Missing projectId or apiKey \u2014 set MUSHI_PROJECT_ID / MUSHI_API_KEY (or EXPO_PUBLIC_* equivalents) or pass them as props."
1112
+ );
1113
+ }
1114
+ }
1016
1115
  const consoleRef = useRef3(null);
1017
1116
  const networkRef = useRef3(null);
1018
1117
  const queueRef = useRef3(null);
1019
1118
  const apiClientRef = useRef3(null);
1119
+ const sessionIdRef = useRef3(`sess_${newUuid()}`);
1120
+ const breadcrumbsRef = useRef3(
1121
+ createBreadcrumbBuffer({ max: config.capture?.maxBreadcrumbs ?? 50 })
1122
+ );
1020
1123
  const [sheetVisible, setSheetVisible] = useState2(false);
1021
1124
  const [sheetScreenshot, setSheetScreenshot] = useState2(null);
1022
1125
  const reporterTokenRef = useRef3(`rn-${config.projectId}-anon`);
@@ -1027,6 +1130,8 @@ function MushiProvider({ children, ...config }) {
1027
1130
  resolveReporterTokenReady = resolve;
1028
1131
  })
1029
1132
  );
1133
+ const userTokenRef = useRef3(null);
1134
+ const pageContextRef = useRef3(null);
1030
1135
  const rawEndpoint = config.apiEndpoint ?? config.endpoint;
1031
1136
  if (rawEndpoint !== void 0 && rawEndpoint.trim() === "") {
1032
1137
  throw new Error(
@@ -1047,7 +1152,8 @@ function MushiProvider({ children, ...config }) {
1047
1152
  apiClientRef.current = createApiClient({
1048
1153
  projectId: config.projectId,
1049
1154
  apiKey: config.apiKey,
1050
- apiEndpoint
1155
+ apiEndpoint,
1156
+ getUserToken: () => userTokenRef.current
1051
1157
  });
1052
1158
  queueRef.current = new AsyncStorageQueue({
1053
1159
  maxSize: config.storage?.maxQueueSize,
@@ -1060,6 +1166,12 @@ function MushiProvider({ children, ...config }) {
1060
1166
  });
1061
1167
  queueRef.current.flush().catch(() => {
1062
1168
  });
1169
+ breadcrumbsRef.current.add({
1170
+ category: "lifecycle",
1171
+ level: "info",
1172
+ message: "Mushi SDK initialised",
1173
+ data: { sdk: MUSHI_SDK_PACKAGE, version: MUSHI_SDK_VERSION }
1174
+ });
1063
1175
  (async () => {
1064
1176
  const TOKEN_KEY = "@mushi:reporter_token";
1065
1177
  try {
@@ -1101,6 +1213,17 @@ function MushiProvider({ children, ...config }) {
1101
1213
  return () => unsubscribe?.();
1102
1214
  }, []);
1103
1215
  const open = useCallback3(() => {
1216
+ breadcrumbsRef.current.add({
1217
+ category: "lifecycle",
1218
+ level: "info",
1219
+ message: "Widget opened",
1220
+ ...screenRef.current?.name ? { data: { screen: screenRef.current.name } } : {}
1221
+ });
1222
+ if (config.capture?.screenshot === false) {
1223
+ setSheetScreenshot(null);
1224
+ setSheetVisible(true);
1225
+ return;
1226
+ }
1104
1227
  let vshot = null;
1105
1228
  try {
1106
1229
  vshot = __require("react-native-view-shot");
@@ -1118,7 +1241,7 @@ function MushiProvider({ children, ...config }) {
1118
1241
  }).finally(() => {
1119
1242
  setSheetVisible(true);
1120
1243
  });
1121
- }, []);
1244
+ }, [config.capture?.screenshot]);
1122
1245
  const close = useCallback3(() => setSheetVisible(false), []);
1123
1246
  const attachTo = useCallback3(() => ({ onPress: open }), [open]);
1124
1247
  useEffect3(() => {
@@ -1152,14 +1275,35 @@ function MushiProvider({ children, ...config }) {
1152
1275
  const submitReport = useCallback3(
1153
1276
  async (data) => {
1154
1277
  const deviceInfo = getDeviceInfo();
1278
+ breadcrumbsRef.current.add({
1279
+ category: "lifecycle",
1280
+ level: "info",
1281
+ message: "Report submitted",
1282
+ data: { category: data.category }
1283
+ });
1155
1284
  const contextMeta = {};
1156
- if (userRef.current?.id) contextMeta.userId = userRef.current.id;
1285
+ if (userRef.current?.id) {
1286
+ contextMeta.user = {
1287
+ id: userRef.current.id,
1288
+ ...userRef.current.email ? { email: userRef.current.email } : {},
1289
+ ...userRef.current.name ? { name: userRef.current.name } : {},
1290
+ ...userRef.current.provider ? { provider: userRef.current.provider } : {}
1291
+ };
1292
+ contextMeta.userId = userRef.current.id;
1293
+ }
1157
1294
  if (userRef.current?.email) contextMeta.userEmail = userRef.current.email;
1158
1295
  if (userRef.current?.name) contextMeta.userName = userRef.current.name;
1159
1296
  if (screenRef.current?.name) contextMeta.screenName = screenRef.current.name;
1160
1297
  if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
1161
1298
  if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
1162
1299
  const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
1300
+ const breadcrumbs = breadcrumbsRef.current.getAll();
1301
+ const timeline = breadcrumbs.map((c) => ({
1302
+ ts: c.timestamp,
1303
+ kind: breadcrumbToTimelineKind(c.category),
1304
+ payload: { message: c.message, level: c.level, ...c.data ?? {} }
1305
+ }));
1306
+ const fingerprintHash = getDeviceFingerprintHash(deviceInfo);
1163
1307
  const report = {
1164
1308
  id: newUuid(),
1165
1309
  projectId: config.projectId,
@@ -1182,7 +1326,14 @@ function MushiProvider({ children, ...config }) {
1182
1326
  })) ?? [],
1183
1327
  screenshotDataUrl: data.screenshotDataUrl,
1184
1328
  metadata: merged,
1329
+ sessionId: sessionIdRef.current,
1185
1330
  reporterToken: reporterTokenRef.current,
1331
+ ...fingerprintHash ? { fingerprintHash } : {},
1332
+ ...deviceInfo.appVersion ? { appVersion: deviceInfo.appVersion } : {},
1333
+ sdkPackage: MUSHI_SDK_PACKAGE,
1334
+ sdkVersion: MUSHI_SDK_VERSION,
1335
+ ...breadcrumbs.length > 0 ? { breadcrumbs } : {},
1336
+ ...timeline.length > 0 ? { timeline } : {},
1186
1337
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1187
1338
  };
1188
1339
  const client = apiClientRef.current;
@@ -1240,11 +1391,53 @@ function MushiProvider({ children, ...config }) {
1240
1391
  provider: traits?.provider
1241
1392
  };
1242
1393
  },
1394
+ setUser(user) {
1395
+ userRef.current = {
1396
+ id: user.id,
1397
+ email: user.email,
1398
+ name: user.name,
1399
+ provider: user.provider
1400
+ };
1401
+ },
1402
+ identifyWithToken(token) {
1403
+ userTokenRef.current = token && typeof token === "string" ? token : null;
1404
+ if (userTokenRef.current) {
1405
+ const claims = parseIdentityToken(userTokenRef.current);
1406
+ if (claims?.sub) {
1407
+ userRef.current = {
1408
+ id: claims.sub,
1409
+ ...claims.email ? { email: claims.email } : {},
1410
+ ...claims.name ? { name: claims.name } : {}
1411
+ };
1412
+ }
1413
+ }
1414
+ },
1415
+ publishPageContext(context) {
1416
+ pageContextRef.current = context && context.route ? context : null;
1417
+ },
1243
1418
  setMetadata(key, value) {
1244
1419
  metadataRef.current[key] = value;
1245
1420
  },
1246
1421
  setScreen(screen) {
1422
+ const prev = screenRef.current?.name;
1247
1423
  screenRef.current = screen;
1424
+ if (screen.name && screen.name !== prev) {
1425
+ breadcrumbsRef.current.add({
1426
+ category: "navigation",
1427
+ level: "info",
1428
+ message: `Navigated to ${screen.name}`,
1429
+ // Always include `route` (fallback to the screen name) so the
1430
+ // admin repro-timeline renders the destination instead of
1431
+ // "unknown route" when the host only set a screen name.
1432
+ data: {
1433
+ route: screen.route ?? screen.name,
1434
+ ...screen.feature ? { feature: screen.feature } : {}
1435
+ }
1436
+ });
1437
+ }
1438
+ },
1439
+ addBreadcrumb(crumb) {
1440
+ breadcrumbsRef.current.add(crumb);
1248
1441
  },
1249
1442
  // Reporter API — scoped to the device's persistent reporter token
1250
1443
  async listMyReports() {
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@mushi-mushi/react-native",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "license": "MIT",
5
5
  "description": "React Native SDK for Mushi Mushi — shake-to-report, bottom sheet widget, full context capture",
6
- "main": "./dist/index.js",
6
+ "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
9
  "peerDependencies": {
@@ -33,7 +33,7 @@
33
33
  }
34
34
  },
35
35
  "dependencies": {
36
- "@mushi-mushi/core": "^1.14.0"
36
+ "@mushi-mushi/core": "^1.16.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/react": "^19.2.14",
@@ -60,6 +60,7 @@
60
60
  "files": [
61
61
  "dist",
62
62
  "README.md",
63
+ "CHANGELOG.md",
63
64
  "LICENSE",
64
65
  "CONTRIBUTING.md",
65
66
  "CODE_OF_CONDUCT.md",