@mushi-mushi/react-native 0.16.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,7 +1,6 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
1
  import * as react from 'react';
3
2
  import { ReactNode, FC, ReactElement } from 'react';
4
- import { MushiRewardsConfig, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
3
+ import { MushiRewardsConfig, MushiPageContext, MushiBreadcrumb, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
5
4
  export { MushiHallOfFameEntry, MushiReporterComment, MushiReporterReport } from '@mushi-mushi/core';
6
5
 
7
6
  interface ConsoleEntry {
@@ -72,8 +71,19 @@ interface MushiRNConfig {
72
71
  console?: boolean;
73
72
  network?: boolean;
74
73
  navigation?: boolean;
74
+ /**
75
+ * Capture a screenshot of the current screen when the widget opens.
76
+ * Defaults to `true`. Requires the optional `react-native-view-shot`
77
+ * peer dependency — when it isn't installed, capture is skipped silently
78
+ * (the report is still sent without an image). The user always sees a
79
+ * thumbnail in the sheet and can remove it before submitting, so
80
+ * sensitive screens (balances, PII) are never sent without consent.
81
+ */
82
+ screenshot?: boolean;
75
83
  maxConsoleEntries?: number;
76
84
  maxNetworkEntries?: number;
85
+ /** Hard cap on the breadcrumb / repro-timeline ring buffer. Default 50. */
86
+ maxBreadcrumbs?: number;
77
87
  };
78
88
  storage?: {
79
89
  maxQueueSize?: number;
@@ -104,6 +114,27 @@ interface MushiRNInstance {
104
114
  provider?: string;
105
115
  [k: string]: unknown;
106
116
  }): void;
117
+ /**
118
+ * Alias for {@link identify} matching the web SDK's `Mushi.setUser()`.
119
+ * Accepts an object so hosts can call `setUser({ id, email, name })`.
120
+ */
121
+ setUser(user: {
122
+ id: string;
123
+ email?: string;
124
+ name?: string;
125
+ provider?: string;
126
+ }): void;
127
+ /**
128
+ * Identify the current end user with a signed Mushi identity JWT minted by
129
+ * the host server. Mirrors the web SDK; the backend verifies the signature
130
+ * before trusting any claim. Pass `null` on logout to go anonymous.
131
+ */
132
+ identifyWithToken(token: string | null): void;
133
+ /**
134
+ * Publish the current screen's context so the assistant can answer
135
+ * page-aware questions. Pass `null` to clear.
136
+ */
137
+ publishPageContext(context: MushiPageContext | null): void;
107
138
  /** Attach arbitrary key/value metadata to subsequent reports. */
108
139
  setMetadata(key: string, value: unknown): void;
109
140
  /** Set the current screen context attached to subsequent reports. */
@@ -112,6 +143,10 @@ interface MushiRNInstance {
112
143
  route?: string;
113
144
  feature?: string;
114
145
  }): void;
146
+ /** Append a breadcrumb to the repro timeline ring buffer. */
147
+ addBreadcrumb(crumb: Omit<MushiBreadcrumb, 'timestamp'> & {
148
+ timestamp?: number;
149
+ }): void;
115
150
  /** List this device's reports ordered by most recent. Returns [] on failure. */
116
151
  listMyReports(): Promise<MushiReporterReport[]>;
117
152
  /** List admin + reporter comments on a specific report. */
@@ -131,9 +166,10 @@ interface MushiRNInstance {
131
166
  /** Returns the current user's reputation + point totals. */
132
167
  getReputation(): Promise<MushiReputationResult | null>;
133
168
  }
134
- declare function MushiProvider({ children, ...config }: MushiRNConfig & {
169
+ declare function MushiProvider({ children, config: configProp, ...barePropConfig }: MushiRNConfig & {
135
170
  children: ReactNode;
136
- }): react_jsx_runtime.JSX.Element;
171
+ config?: Partial<MushiRNConfig>;
172
+ }): react.JSX.Element;
137
173
 
138
174
  declare function useMushi(): MushiRNInstance | null;
139
175
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
1
  import * as react from 'react';
3
2
  import { ReactNode, FC, ReactElement } from 'react';
4
- import { MushiRewardsConfig, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
3
+ import { MushiRewardsConfig, MushiPageContext, MushiBreadcrumb, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
5
4
  export { MushiHallOfFameEntry, MushiReporterComment, MushiReporterReport } from '@mushi-mushi/core';
6
5
 
7
6
  interface ConsoleEntry {
@@ -72,8 +71,19 @@ interface MushiRNConfig {
72
71
  console?: boolean;
73
72
  network?: boolean;
74
73
  navigation?: boolean;
74
+ /**
75
+ * Capture a screenshot of the current screen when the widget opens.
76
+ * Defaults to `true`. Requires the optional `react-native-view-shot`
77
+ * peer dependency — when it isn't installed, capture is skipped silently
78
+ * (the report is still sent without an image). The user always sees a
79
+ * thumbnail in the sheet and can remove it before submitting, so
80
+ * sensitive screens (balances, PII) are never sent without consent.
81
+ */
82
+ screenshot?: boolean;
75
83
  maxConsoleEntries?: number;
76
84
  maxNetworkEntries?: number;
85
+ /** Hard cap on the breadcrumb / repro-timeline ring buffer. Default 50. */
86
+ maxBreadcrumbs?: number;
77
87
  };
78
88
  storage?: {
79
89
  maxQueueSize?: number;
@@ -104,6 +114,27 @@ interface MushiRNInstance {
104
114
  provider?: string;
105
115
  [k: string]: unknown;
106
116
  }): void;
117
+ /**
118
+ * Alias for {@link identify} matching the web SDK's `Mushi.setUser()`.
119
+ * Accepts an object so hosts can call `setUser({ id, email, name })`.
120
+ */
121
+ setUser(user: {
122
+ id: string;
123
+ email?: string;
124
+ name?: string;
125
+ provider?: string;
126
+ }): void;
127
+ /**
128
+ * Identify the current end user with a signed Mushi identity JWT minted by
129
+ * the host server. Mirrors the web SDK; the backend verifies the signature
130
+ * before trusting any claim. Pass `null` on logout to go anonymous.
131
+ */
132
+ identifyWithToken(token: string | null): void;
133
+ /**
134
+ * Publish the current screen's context so the assistant can answer
135
+ * page-aware questions. Pass `null` to clear.
136
+ */
137
+ publishPageContext(context: MushiPageContext | null): void;
107
138
  /** Attach arbitrary key/value metadata to subsequent reports. */
108
139
  setMetadata(key: string, value: unknown): void;
109
140
  /** Set the current screen context attached to subsequent reports. */
@@ -112,6 +143,10 @@ interface MushiRNInstance {
112
143
  route?: string;
113
144
  feature?: string;
114
145
  }): void;
146
+ /** Append a breadcrumb to the repro timeline ring buffer. */
147
+ addBreadcrumb(crumb: Omit<MushiBreadcrumb, 'timestamp'> & {
148
+ timestamp?: number;
149
+ }): void;
115
150
  /** List this device's reports ordered by most recent. Returns [] on failure. */
116
151
  listMyReports(): Promise<MushiReporterReport[]>;
117
152
  /** List admin + reporter comments on a specific report. */
@@ -131,9 +166,10 @@ interface MushiRNInstance {
131
166
  /** Returns the current user's reputation + point totals. */
132
167
  getReputation(): Promise<MushiReputationResult | null>;
133
168
  }
134
- declare function MushiProvider({ children, ...config }: MushiRNConfig & {
169
+ declare function MushiProvider({ children, config: configProp, ...barePropConfig }: MushiRNConfig & {
135
170
  children: ReactNode;
136
- }): react_jsx_runtime.JSX.Element;
171
+ config?: Partial<MushiRNConfig>;
172
+ }): react.JSX.Element;
137
173
 
138
174
  declare function useMushi(): MushiRNInstance | null;
139
175
 
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
  ]
@@ -654,7 +691,13 @@ var MushiBottomSheet = ({
654
691
  var s = StyleSheet.create({
655
692
  flex: { flex: 1 },
656
693
  backdrop: {
657
- ...StyleSheet.absoluteFillObject
694
+ // Inlined StyleSheet.absoluteFillObject — RN 0.86 dropped it from the
695
+ // StyleSheet TS types; the literal is equivalent and works on all RN >=0.72.
696
+ position: "absolute",
697
+ top: 0,
698
+ left: 0,
699
+ right: 0,
700
+ bottom: 0
658
701
  },
659
702
  sheet: {
660
703
  position: "absolute",
@@ -681,6 +724,24 @@ var s = StyleSheet.create({
681
724
  borderRadius: 3,
682
725
  opacity: 0.5
683
726
  },
727
+ brandHeader: {
728
+ paddingHorizontal: 20,
729
+ paddingTop: 10,
730
+ paddingBottom: 12,
731
+ borderBottomWidth: 1.5
732
+ },
733
+ brandEyebrow: {
734
+ fontSize: 10,
735
+ fontWeight: "700",
736
+ letterSpacing: 1.2,
737
+ marginBottom: 2,
738
+ opacity: 0.7
739
+ },
740
+ brandTitle: {
741
+ fontSize: 18,
742
+ fontWeight: "800",
743
+ letterSpacing: -0.2
744
+ },
684
745
  body: {
685
746
  paddingHorizontal: 20,
686
747
  paddingTop: 4
@@ -690,6 +751,14 @@ var s = StyleSheet.create({
690
751
  fontWeight: "700",
691
752
  marginBottom: 16
692
753
  },
754
+ stepLabel: {
755
+ fontSize: 12,
756
+ fontWeight: "600",
757
+ letterSpacing: 0.2,
758
+ marginTop: 8,
759
+ marginBottom: 10,
760
+ textTransform: "uppercase"
761
+ },
693
762
  catRow: {
694
763
  flexDirection: "row",
695
764
  justifyContent: "space-between",
@@ -825,6 +894,7 @@ import {
825
894
  Dimensions as Dimensions3,
826
895
  useColorScheme as useColorScheme2
827
896
  } from "react-native";
897
+ import { mushiPalette as mushiPalette2, MUSHI_COPY } from "@mushi-mushi/core";
828
898
  import { jsx as jsx2 } from "react/jsx-runtime";
829
899
  var FAB_SIZE = 56;
830
900
  var EDGE_PADDING = 12;
@@ -876,7 +946,7 @@ var MushiFloatingButton = ({
876
946
  const currentX = useRef2(baseX);
877
947
  const currentY = useRef2(0);
878
948
  const isDragging = useRef2(false);
879
- const accentColor = accent ?? (dark ? "#0a84ff" : "#007aff");
949
+ const accentColor = accent ?? mushiPalette2(dark ? "dark" : "light").accent;
880
950
  useEffect2(() => {
881
951
  if (!persist) return;
882
952
  void loadStoredPos().then((pos) => {
@@ -986,7 +1056,7 @@ var MushiFloatingButton = ({
986
1056
  onStartShouldSetResponder: () => !draggable,
987
1057
  onResponderGrant: handlePressIn,
988
1058
  onResponderRelease: handlePressOut,
989
- children: /* @__PURE__ */ jsx2(Animated2.Text, { style: s2.emoji, children: "\u{1F41B}" })
1059
+ children: /* @__PURE__ */ jsx2(Animated2.Text, { style: s2.emoji, children: MUSHI_COPY.triggerText })
990
1060
  }
991
1061
  );
992
1062
  };
@@ -1009,14 +1079,53 @@ var s2 = StyleSheet2.create({
1009
1079
  }
1010
1080
  });
1011
1081
 
1082
+ // src/version.ts
1083
+ var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
1084
+ var MUSHI_SDK_VERSION = "0.18.1" ? "0.18.1" : "0.17.0";
1085
+
1012
1086
  // src/provider.tsx
1013
1087
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1014
1088
  var MushiContext = createContext(null);
1015
- function MushiProvider({ children, ...config }) {
1089
+ function breadcrumbToTimelineKind(category) {
1090
+ switch (category) {
1091
+ case "navigation":
1092
+ return "route";
1093
+ case "ui.click":
1094
+ case "ui.tap":
1095
+ return "click";
1096
+ case "xhr":
1097
+ case "fetch":
1098
+ case "network":
1099
+ return "request";
1100
+ default:
1101
+ return "log";
1102
+ }
1103
+ }
1104
+ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1105
+ const proc = globalThis.process;
1106
+ const envProjectId = proc?.env?.MUSHI_PROJECT_ID || proc?.env?.EXPO_PUBLIC_MUSHI_PROJECT_ID;
1107
+ const envApiKey = proc?.env?.MUSHI_API_KEY || proc?.env?.EXPO_PUBLIC_MUSHI_API_KEY;
1108
+ const config = {
1109
+ ...envProjectId ? { projectId: envProjectId } : {},
1110
+ ...envApiKey ? { apiKey: envApiKey } : {},
1111
+ ...configProp,
1112
+ ...barePropConfig
1113
+ };
1114
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
1115
+ if (!config.projectId || !config.apiKey) {
1116
+ console.warn(
1117
+ "[MushiProvider] Missing projectId or apiKey \u2014 set MUSHI_PROJECT_ID / MUSHI_API_KEY (or EXPO_PUBLIC_* equivalents) or pass them as props."
1118
+ );
1119
+ }
1120
+ }
1016
1121
  const consoleRef = useRef3(null);
1017
1122
  const networkRef = useRef3(null);
1018
1123
  const queueRef = useRef3(null);
1019
1124
  const apiClientRef = useRef3(null);
1125
+ const sessionIdRef = useRef3(`sess_${newUuid()}`);
1126
+ const breadcrumbsRef = useRef3(
1127
+ createBreadcrumbBuffer({ max: config.capture?.maxBreadcrumbs ?? 50 })
1128
+ );
1020
1129
  const [sheetVisible, setSheetVisible] = useState2(false);
1021
1130
  const [sheetScreenshot, setSheetScreenshot] = useState2(null);
1022
1131
  const reporterTokenRef = useRef3(`rn-${config.projectId}-anon`);
@@ -1027,6 +1136,8 @@ function MushiProvider({ children, ...config }) {
1027
1136
  resolveReporterTokenReady = resolve;
1028
1137
  })
1029
1138
  );
1139
+ const userTokenRef = useRef3(null);
1140
+ const pageContextRef = useRef3(null);
1030
1141
  const rawEndpoint = config.apiEndpoint ?? config.endpoint;
1031
1142
  if (rawEndpoint !== void 0 && rawEndpoint.trim() === "") {
1032
1143
  throw new Error(
@@ -1047,7 +1158,10 @@ function MushiProvider({ children, ...config }) {
1047
1158
  apiClientRef.current = createApiClient({
1048
1159
  projectId: config.projectId,
1049
1160
  apiKey: config.apiKey,
1050
- apiEndpoint
1161
+ apiEndpoint,
1162
+ getUserToken: () => userTokenRef.current,
1163
+ sdkPackage: MUSHI_SDK_PACKAGE,
1164
+ sdkVersion: MUSHI_SDK_VERSION
1051
1165
  });
1052
1166
  queueRef.current = new AsyncStorageQueue({
1053
1167
  maxSize: config.storage?.maxQueueSize,
@@ -1060,6 +1174,12 @@ function MushiProvider({ children, ...config }) {
1060
1174
  });
1061
1175
  queueRef.current.flush().catch(() => {
1062
1176
  });
1177
+ breadcrumbsRef.current.add({
1178
+ category: "lifecycle",
1179
+ level: "info",
1180
+ message: "Mushi SDK initialised",
1181
+ data: { sdk: MUSHI_SDK_PACKAGE, version: MUSHI_SDK_VERSION }
1182
+ });
1063
1183
  (async () => {
1064
1184
  const TOKEN_KEY = "@mushi:reporter_token";
1065
1185
  try {
@@ -1101,6 +1221,17 @@ function MushiProvider({ children, ...config }) {
1101
1221
  return () => unsubscribe?.();
1102
1222
  }, []);
1103
1223
  const open = useCallback3(() => {
1224
+ breadcrumbsRef.current.add({
1225
+ category: "lifecycle",
1226
+ level: "info",
1227
+ message: "Widget opened",
1228
+ ...screenRef.current?.name ? { data: { screen: screenRef.current.name } } : {}
1229
+ });
1230
+ if (config.capture?.screenshot === false) {
1231
+ setSheetScreenshot(null);
1232
+ setSheetVisible(true);
1233
+ return;
1234
+ }
1104
1235
  let vshot = null;
1105
1236
  try {
1106
1237
  vshot = __require("react-native-view-shot");
@@ -1118,7 +1249,7 @@ function MushiProvider({ children, ...config }) {
1118
1249
  }).finally(() => {
1119
1250
  setSheetVisible(true);
1120
1251
  });
1121
- }, []);
1252
+ }, [config.capture?.screenshot]);
1122
1253
  const close = useCallback3(() => setSheetVisible(false), []);
1123
1254
  const attachTo = useCallback3(() => ({ onPress: open }), [open]);
1124
1255
  useEffect3(() => {
@@ -1152,14 +1283,35 @@ function MushiProvider({ children, ...config }) {
1152
1283
  const submitReport = useCallback3(
1153
1284
  async (data) => {
1154
1285
  const deviceInfo = getDeviceInfo();
1286
+ breadcrumbsRef.current.add({
1287
+ category: "lifecycle",
1288
+ level: "info",
1289
+ message: "Report submitted",
1290
+ data: { category: data.category }
1291
+ });
1155
1292
  const contextMeta = {};
1156
- if (userRef.current?.id) contextMeta.userId = userRef.current.id;
1293
+ if (userRef.current?.id) {
1294
+ contextMeta.user = {
1295
+ id: userRef.current.id,
1296
+ ...userRef.current.email ? { email: userRef.current.email } : {},
1297
+ ...userRef.current.name ? { name: userRef.current.name } : {},
1298
+ ...userRef.current.provider ? { provider: userRef.current.provider } : {}
1299
+ };
1300
+ contextMeta.userId = userRef.current.id;
1301
+ }
1157
1302
  if (userRef.current?.email) contextMeta.userEmail = userRef.current.email;
1158
1303
  if (userRef.current?.name) contextMeta.userName = userRef.current.name;
1159
1304
  if (screenRef.current?.name) contextMeta.screenName = screenRef.current.name;
1160
1305
  if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
1161
1306
  if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
1162
1307
  const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
1308
+ const breadcrumbs = breadcrumbsRef.current.getAll();
1309
+ const timeline = breadcrumbs.map((c) => ({
1310
+ ts: c.timestamp,
1311
+ kind: breadcrumbToTimelineKind(c.category),
1312
+ payload: { message: c.message, level: c.level, ...c.data ?? {} }
1313
+ }));
1314
+ const fingerprintHash = getDeviceFingerprintHash(deviceInfo);
1163
1315
  const report = {
1164
1316
  id: newUuid(),
1165
1317
  projectId: config.projectId,
@@ -1182,7 +1334,14 @@ function MushiProvider({ children, ...config }) {
1182
1334
  })) ?? [],
1183
1335
  screenshotDataUrl: data.screenshotDataUrl,
1184
1336
  metadata: merged,
1337
+ sessionId: sessionIdRef.current,
1185
1338
  reporterToken: reporterTokenRef.current,
1339
+ ...fingerprintHash ? { fingerprintHash } : {},
1340
+ ...deviceInfo.appVersion ? { appVersion: deviceInfo.appVersion } : {},
1341
+ sdkPackage: MUSHI_SDK_PACKAGE,
1342
+ sdkVersion: MUSHI_SDK_VERSION,
1343
+ ...breadcrumbs.length > 0 ? { breadcrumbs } : {},
1344
+ ...timeline.length > 0 ? { timeline } : {},
1186
1345
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1187
1346
  };
1188
1347
  const client = apiClientRef.current;
@@ -1240,11 +1399,53 @@ function MushiProvider({ children, ...config }) {
1240
1399
  provider: traits?.provider
1241
1400
  };
1242
1401
  },
1402
+ setUser(user) {
1403
+ userRef.current = {
1404
+ id: user.id,
1405
+ email: user.email,
1406
+ name: user.name,
1407
+ provider: user.provider
1408
+ };
1409
+ },
1410
+ identifyWithToken(token) {
1411
+ userTokenRef.current = token && typeof token === "string" ? token : null;
1412
+ if (userTokenRef.current) {
1413
+ const claims = parseIdentityToken(userTokenRef.current);
1414
+ if (claims?.sub) {
1415
+ userRef.current = {
1416
+ id: claims.sub,
1417
+ ...claims.email ? { email: claims.email } : {},
1418
+ ...claims.name ? { name: claims.name } : {}
1419
+ };
1420
+ }
1421
+ }
1422
+ },
1423
+ publishPageContext(context) {
1424
+ pageContextRef.current = context && context.route ? context : null;
1425
+ },
1243
1426
  setMetadata(key, value) {
1244
1427
  metadataRef.current[key] = value;
1245
1428
  },
1246
1429
  setScreen(screen) {
1430
+ const prev = screenRef.current?.name;
1247
1431
  screenRef.current = screen;
1432
+ if (screen.name && screen.name !== prev) {
1433
+ breadcrumbsRef.current.add({
1434
+ category: "navigation",
1435
+ level: "info",
1436
+ message: `Navigated to ${screen.name}`,
1437
+ // Always include `route` (fallback to the screen name) so the
1438
+ // admin repro-timeline renders the destination instead of
1439
+ // "unknown route" when the host only set a screen name.
1440
+ data: {
1441
+ route: screen.route ?? screen.name,
1442
+ ...screen.feature ? { feature: screen.feature } : {}
1443
+ }
1444
+ });
1445
+ }
1446
+ },
1447
+ addBreadcrumb(crumb) {
1448
+ breadcrumbsRef.current.add(crumb);
1248
1449
  },
1249
1450
  // Reporter API — scoped to the device's persistent reporter token
1250
1451
  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.18.1",
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,15 +33,15 @@
33
33
  }
34
34
  },
35
35
  "dependencies": {
36
- "@mushi-mushi/core": "^1.14.0"
36
+ "@mushi-mushi/core": "^1.17.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/react": "^19.2.14",
40
- "react": "^19.2.5",
41
- "react-native": "^0.79.7",
39
+ "@types/react": "^19.2.17",
40
+ "react": "^19.2.7",
41
+ "react-native": "^0.86.0",
42
42
  "tsup": "^8.5.1",
43
43
  "typescript": "^6.0.3",
44
- "vitest": "^4.1.5"
44
+ "vitest": "^4.1.8"
45
45
  },
46
46
  "author": "Kenji Sakuramoto",
47
47
  "repository": {
@@ -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",