@mushi-mushi/react-native 0.15.2 → 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.js CHANGED
@@ -9,7 +9,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
9
9
  import {
10
10
  createContext,
11
11
  useContext,
12
- useEffect as useEffect2,
12
+ useEffect as useEffect3,
13
13
  useRef as useRef3,
14
14
  useState as useState2,
15
15
  useCallback as useCallback3,
@@ -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 {
@@ -306,12 +332,20 @@ import {
306
332
  Dimensions as Dimensions2,
307
333
  Platform as Platform2,
308
334
  KeyboardAvoidingView,
335
+ ScrollView,
309
336
  useColorScheme
310
337
  } from "react-native";
338
+ import { mushiPalette } from "@mushi-mushi/core";
311
339
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
312
340
  var { height: SCREEN_HEIGHT } = Dimensions2.get("window");
313
341
  var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
314
342
  var DISMISS_THRESHOLD = 80;
343
+ var NEON = {
344
+ accent: "#0FFF50",
345
+ accentHover: "#00E646",
346
+ ink: "#0a1a0a",
347
+ border: "#00C43A"
348
+ };
315
349
  var CATEGORIES = [
316
350
  { key: "bug", emoji: "\u{1F41B}", label: "Bug" },
317
351
  { key: "slow", emoji: "\u{1F422}", label: "Slow" },
@@ -458,13 +492,15 @@ var MushiBottomSheet = ({
458
492
  }
459
493
  };
460
494
  const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
461
- 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)" };
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 };
462
497
  const canSubmit = !!category && description.trim().length > 0 && phase === "form";
463
498
  return /* @__PURE__ */ jsx(Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ jsxs(
464
499
  KeyboardAvoidingView,
465
500
  {
466
501
  style: s.flex,
467
- behavior: Platform2.OS === "ios" ? "padding" : void 0,
502
+ behavior: Platform2.OS === "ios" ? "padding" : "height",
503
+ keyboardVerticalOffset: Platform2.OS === "ios" ? 0 : 20,
468
504
  children: [
469
505
  /* @__PURE__ */ jsx(
470
506
  Animated.View,
@@ -482,7 +518,11 @@ var MushiBottomSheet = ({
482
518
  ],
483
519
  children: [
484
520
  /* @__PURE__ */ jsx(View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ jsx(View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
485
- /* @__PURE__ */ jsx(View, { style: s.tabRow, children: ["report", "inbox"].map((tab) => /* @__PURE__ */ jsx(
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
+ ] }),
525
+ /* @__PURE__ */ jsx(View, { style: s.tabRow, children: ["report", "inbox", "community"].map((tab) => /* @__PURE__ */ jsx(
486
526
  TouchableOpacity,
487
527
  {
488
528
  onPress: () => {
@@ -491,11 +531,11 @@ var MushiBottomSheet = ({
491
531
  else setSelectedReportId(null);
492
532
  },
493
533
  style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
494
- children: /* @__PURE__ */ jsx(Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.accent : colors.sub }], children: tab === "report" ? "Report" : "Inbox" })
534
+ children: /* @__PURE__ */ jsx(Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.text : colors.sub }], children: tab === "report" ? "Report" : tab === "inbox" ? "Your reports" : "Community" })
495
535
  },
496
536
  tab
497
537
  )) }),
498
- sheetTab === "inbox" ? /* @__PURE__ */ jsx(View, { style: s.body, children: inboxLoading ? /* @__PURE__ */ jsx(Text, { style: { color: colors.sub }, children: "Loading\u2026" }) : selectedReportId ? /* @__PURE__ */ jsxs(Fragment, { children: [
538
+ sheetTab === "inbox" ? /* @__PURE__ */ jsx(ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: inboxLoading ? /* @__PURE__ */ jsx(Text, { style: { color: colors.sub }, children: "Loading\u2026" }) : selectedReportId ? /* @__PURE__ */ jsxs(Fragment, { children: [
499
539
  /* @__PURE__ */ jsx(TouchableOpacity, { onPress: () => setSelectedReportId(null), children: /* @__PURE__ */ jsx(Text, { style: { color: colors.accent, marginBottom: 8 }, children: "\u2190 Back" }) }),
500
540
  threadComments.map((c) => /* @__PURE__ */ jsxs(View, { style: [s.threadBubble, { backgroundColor: colors.card }], children: [
501
541
  /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: c.author_kind }),
@@ -504,7 +544,7 @@ var MushiBottomSheet = ({
504
544
  VERIFIABLE_STATUSES.has(
505
545
  inboxReports.find((r) => r.id === selectedReportId)?.status ?? ""
506
546
  ) && /* @__PURE__ */ jsxs(View, { style: s.verifyRow, children: [
507
- /* @__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" }) }),
508
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" }) })
509
549
  ] }),
510
550
  /* @__PURE__ */ jsx(
@@ -517,106 +557,130 @@ var MushiBottomSheet = ({
517
557
  onChangeText: setReplyText
518
558
  }
519
559
  ),
520
- /* @__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" }) })
521
561
  ] }) : inboxReports.map((r) => /* @__PURE__ */ jsxs(TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
522
562
  /* @__PURE__ */ jsx(Text, { style: { color: colors.text, fontWeight: "600" }, numberOfLines: 1, children: (r.summary ?? r.description ?? "Report").slice(0, 60) }),
523
563
  /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: r.status })
524
- ] }, r.id)) }) : phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
564
+ ] }, r.id)) }) : sheetTab === "community" ? /* @__PURE__ */ jsxs(ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: [
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." }),
566
+ /* @__PURE__ */ jsx(
567
+ TextInput,
568
+ {
569
+ style: [s.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border, minHeight: 48 }],
570
+ placeholder: "you@example.com",
571
+ placeholderTextColor: colors.sub,
572
+ keyboardType: "email-address",
573
+ autoCapitalize: "none",
574
+ autoCorrect: false,
575
+ textContentType: "emailAddress"
576
+ }
577
+ ),
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" }) }),
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." })
580
+ ] }) : phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
525
581
  /* @__PURE__ */ jsx(Text, { style: [s.sentEmoji], children: "\u2705" }),
526
582
  /* @__PURE__ */ jsx(Text, { style: [s.sentText, { color: colors.text }], children: "Report sent!" })
527
- ] }) : /* @__PURE__ */ jsxs(View, { style: s.body, children: [
528
- /* @__PURE__ */ jsx(Text, { style: [s.title, { color: colors.text }], children: "Report an issue" }),
529
- /* @__PURE__ */ jsx(View, { style: s.catRow, children: CATEGORIES.map((c) => {
530
- const active = category === c.key;
531
- return /* @__PURE__ */ jsxs(
532
- TouchableOpacity,
533
- {
534
- onPress: () => setCategory(c.key),
535
- activeOpacity: 0.7,
536
- style: [
537
- s.catBtn,
583
+ ] }) : /* @__PURE__ */ jsxs(
584
+ ScrollView,
585
+ {
586
+ style: s.body,
587
+ keyboardShouldPersistTaps: "handled",
588
+ contentContainerStyle: { paddingBottom: 24 },
589
+ children: [
590
+ /* @__PURE__ */ jsx(Text, { style: [s.stepLabel, { color: colors.sub }], children: "What kind of issue?" }),
591
+ /* @__PURE__ */ jsx(View, { style: s.catRow, children: CATEGORIES.map((c) => {
592
+ const active = category === c.key;
593
+ return /* @__PURE__ */ jsxs(
594
+ TouchableOpacity,
538
595
  {
539
- backgroundColor: active ? colors.accent : colors.card,
540
- borderColor: active ? colors.accent : colors.border
541
- }
542
- ],
543
- children: [
544
- /* @__PURE__ */ jsx(Text, { style: s.catEmoji, children: c.emoji }),
545
- /* @__PURE__ */ jsx(
546
- Text,
596
+ onPress: () => setCategory(c.key),
597
+ activeOpacity: 0.7,
598
+ style: [
599
+ s.catBtn,
600
+ {
601
+ backgroundColor: active ? colors.accent : colors.card,
602
+ borderColor: active ? colors.accent : colors.border
603
+ }
604
+ ],
605
+ children: [
606
+ /* @__PURE__ */ jsx(Text, { style: s.catEmoji, children: c.emoji }),
607
+ /* @__PURE__ */ jsx(
608
+ Text,
609
+ {
610
+ style: [
611
+ s.catLabel,
612
+ { color: active ? colors.accentInk : colors.text }
613
+ ],
614
+ children: c.label
615
+ }
616
+ )
617
+ ]
618
+ },
619
+ c.key
620
+ );
621
+ }) }),
622
+ /* @__PURE__ */ jsx(
623
+ TextInput,
624
+ {
625
+ style: [
626
+ s.input,
547
627
  {
548
- style: [
549
- s.catLabel,
550
- { color: active ? "#fff" : colors.text }
551
- ],
552
- children: c.label
628
+ backgroundColor: colors.card,
629
+ color: colors.text,
630
+ borderColor: colors.border
553
631
  }
554
- )
555
- ]
556
- },
557
- c.key
558
- );
559
- }) }),
560
- /* @__PURE__ */ jsx(
561
- TextInput,
562
- {
563
- style: [
564
- s.input,
632
+ ],
633
+ placeholder: "What happened?",
634
+ placeholderTextColor: colors.sub,
635
+ multiline: true,
636
+ textAlignVertical: "top",
637
+ value: description,
638
+ onChangeText: setDescription,
639
+ editable: phase === "form"
640
+ }
641
+ ),
642
+ activeScreenshot ? /* @__PURE__ */ jsxs(View, { style: s.screenshotRow, children: [
643
+ /* @__PURE__ */ jsx(
644
+ Image,
645
+ {
646
+ source: { uri: activeScreenshot },
647
+ style: s.screenshotThumb,
648
+ accessibilityLabel: "Attached screenshot"
649
+ }
650
+ ),
651
+ /* @__PURE__ */ jsxs(View, { style: s.screenshotMeta, children: [
652
+ /* @__PURE__ */ jsx(Text, { style: [s.screenshotLabel, { color: colors.text }], children: "Screenshot attached" }),
653
+ /* @__PURE__ */ jsx(Text, { style: [s.screenshotSub, { color: colors.sub }], children: "Helps the team see the issue" })
654
+ ] }),
655
+ /* @__PURE__ */ jsx(
656
+ TouchableOpacity,
657
+ {
658
+ onPress: () => {
659
+ setScreenshotAttached(false);
660
+ onClearScreenshot?.();
661
+ },
662
+ hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
663
+ accessibilityLabel: "Remove screenshot",
664
+ children: /* @__PURE__ */ jsx(Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
665
+ }
666
+ )
667
+ ] }) : null,
668
+ /* @__PURE__ */ jsx(
669
+ TouchableOpacity,
565
670
  {
566
- backgroundColor: colors.card,
567
- color: colors.text,
568
- borderColor: colors.border
671
+ onPress: handleSubmit,
672
+ disabled: !canSubmit,
673
+ activeOpacity: 0.8,
674
+ style: [
675
+ s.submitBtn,
676
+ { backgroundColor: canSubmit ? colors.accent : colors.disabled }
677
+ ],
678
+ children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: canSubmit ? colors.accentInk : colors.disabledText }], children: phase === "sending" ? "Sending\u2026" : "Submit report" })
569
679
  }
570
- ],
571
- placeholder: "What happened?",
572
- placeholderTextColor: colors.sub,
573
- multiline: true,
574
- textAlignVertical: "top",
575
- value: description,
576
- onChangeText: setDescription,
577
- editable: phase === "form"
578
- }
579
- ),
580
- activeScreenshot ? /* @__PURE__ */ jsxs(View, { style: s.screenshotRow, children: [
581
- /* @__PURE__ */ jsx(
582
- Image,
583
- {
584
- source: { uri: activeScreenshot },
585
- style: s.screenshotThumb,
586
- accessibilityLabel: "Attached screenshot"
587
- }
588
- ),
589
- /* @__PURE__ */ jsxs(View, { style: s.screenshotMeta, children: [
590
- /* @__PURE__ */ jsx(Text, { style: [s.screenshotLabel, { color: colors.text }], children: "Screenshot attached" }),
591
- /* @__PURE__ */ jsx(Text, { style: [s.screenshotSub, { color: colors.sub }], children: "Helps the team see the issue" })
592
- ] }),
593
- /* @__PURE__ */ jsx(
594
- TouchableOpacity,
595
- {
596
- onPress: () => {
597
- setScreenshotAttached(false);
598
- onClearScreenshot?.();
599
- },
600
- hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
601
- accessibilityLabel: "Remove screenshot",
602
- children: /* @__PURE__ */ jsx(Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
603
- }
604
- )
605
- ] }) : null,
606
- /* @__PURE__ */ jsx(
607
- TouchableOpacity,
608
- {
609
- onPress: handleSubmit,
610
- disabled: !canSubmit,
611
- activeOpacity: 0.8,
612
- style: [
613
- s.submitBtn,
614
- { backgroundColor: canSubmit ? colors.accent : colors.border }
615
- ],
616
- children: /* @__PURE__ */ jsx(Text, { style: s.submitText, children: phase === "sending" ? "Sending\u2026" : "Submit" })
617
- }
618
- )
619
- ] })
680
+ )
681
+ ]
682
+ }
683
+ )
620
684
  ]
621
685
  }
622
686
  )
@@ -654,6 +718,24 @@ var s = StyleSheet.create({
654
718
  borderRadius: 3,
655
719
  opacity: 0.5
656
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
+ },
657
739
  body: {
658
740
  paddingHorizontal: 20,
659
741
  paddingTop: 4
@@ -663,6 +745,14 @@ var s = StyleSheet.create({
663
745
  fontWeight: "700",
664
746
  marginBottom: 16
665
747
  },
748
+ stepLabel: {
749
+ fontSize: 12,
750
+ fontWeight: "600",
751
+ letterSpacing: 0.2,
752
+ marginTop: 8,
753
+ marginBottom: 10,
754
+ textTransform: "uppercase"
755
+ },
666
756
  catRow: {
667
757
  flexDirection: "row",
668
758
  justifyContent: "space-between",
@@ -789,24 +879,144 @@ var s = StyleSheet.create({
789
879
  });
790
880
 
791
881
  // src/components/MushiFloatingButton.tsx
792
- import { useRef as useRef2, useCallback as useCallback2 } from "react";
882
+ import { useRef as useRef2, useCallback as useCallback2, useEffect as useEffect2 } from "react";
793
883
  import {
794
884
  Animated as Animated2,
795
- TouchableWithoutFeedback,
885
+ PanResponder as PanResponder2,
796
886
  StyleSheet as StyleSheet2,
797
887
  Platform as Platform3,
888
+ Dimensions as Dimensions3,
798
889
  useColorScheme as useColorScheme2
799
890
  } from "react-native";
891
+ import { mushiPalette as mushiPalette2, MUSHI_COPY } from "@mushi-mushi/core";
800
892
  import { jsx as jsx2 } from "react/jsx-runtime";
893
+ var FAB_SIZE = 56;
894
+ var EDGE_PADDING = 12;
895
+ function getScreenBounds() {
896
+ const { width, height } = Dimensions3.get("window");
897
+ return { width, height };
898
+ }
899
+ function clamp(value, min, max) {
900
+ return Math.min(Math.max(value, min), max);
901
+ }
902
+ var STORAGE_KEY = "mushi:fab-pos";
903
+ async function loadStoredPos() {
904
+ try {
905
+ const AsyncStorage = (await import("@react-native-async-storage/async-storage")).default;
906
+ const raw = await AsyncStorage.getItem(STORAGE_KEY);
907
+ if (!raw) return null;
908
+ return JSON.parse(raw);
909
+ } catch {
910
+ return null;
911
+ }
912
+ }
913
+ async function storePos(pos) {
914
+ try {
915
+ const AsyncStorage = (await import("@react-native-async-storage/async-storage")).default;
916
+ await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(pos));
917
+ } catch {
918
+ }
919
+ }
801
920
  var MushiFloatingButton = ({
802
921
  onPress,
803
922
  position = "bottom-right",
804
- inset
923
+ inset,
924
+ config,
925
+ accent
805
926
  }) => {
806
927
  const scheme = useColorScheme2();
807
928
  const dark = scheme === "dark";
929
+ const draggable = config?.draggable ?? false;
930
+ const snapToEdge = config?.snapToEdge ?? true;
931
+ const persist = config?.persist ?? false;
932
+ const baseBottom = inset?.bottom ?? (Platform3.OS === "ios" ? 50 : 28);
933
+ const baseX = position === "bottom-left" ? inset?.left ?? EDGE_PADDING : (() => {
934
+ const { width } = getScreenBounds();
935
+ return width - FAB_SIZE - (inset?.right ?? EDGE_PADDING);
936
+ })();
808
937
  const scale = useRef2(new Animated2.Value(1)).current;
938
+ const posX = useRef2(new Animated2.Value(baseX)).current;
939
+ const posY = useRef2(new Animated2.Value(0)).current;
940
+ const currentX = useRef2(baseX);
941
+ const currentY = useRef2(0);
942
+ const isDragging = useRef2(false);
943
+ const accentColor = accent ?? mushiPalette2(dark ? "dark" : "light").accent;
944
+ useEffect2(() => {
945
+ if (!persist) return;
946
+ void loadStoredPos().then((pos) => {
947
+ if (!pos) return;
948
+ currentX.current = pos.x;
949
+ currentY.current = pos.y;
950
+ posX.setValue(pos.x);
951
+ posY.setValue(pos.y);
952
+ });
953
+ }, [persist, posX, posY]);
954
+ const snapAndPersist = useCallback2(
955
+ (x, y) => {
956
+ const { width, height } = getScreenBounds();
957
+ let snappedX = x;
958
+ if (snapToEdge) {
959
+ const midX = width / 2;
960
+ snappedX = x + FAB_SIZE / 2 < midX ? EDGE_PADDING : width - FAB_SIZE - EDGE_PADDING;
961
+ }
962
+ const clampedX = clamp(snappedX, EDGE_PADDING, width - FAB_SIZE - EDGE_PADDING);
963
+ const clampedY = clamp(y, -(height - FAB_SIZE - baseBottom - 60), 0);
964
+ currentX.current = clampedX;
965
+ currentY.current = clampedY;
966
+ Animated2.spring(posX, {
967
+ toValue: clampedX,
968
+ useNativeDriver: false,
969
+ damping: 18,
970
+ stiffness: 220
971
+ }).start();
972
+ Animated2.spring(posY, {
973
+ toValue: clampedY,
974
+ useNativeDriver: false,
975
+ damping: 18,
976
+ stiffness: 220
977
+ }).start();
978
+ if (persist) void storePos({ x: clampedX, y: clampedY });
979
+ },
980
+ [snapToEdge, persist, posX, posY, baseBottom]
981
+ );
982
+ const panResponder = useRef2(
983
+ draggable ? PanResponder2.create({
984
+ onStartShouldSetPanResponder: () => true,
985
+ onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 6 || Math.abs(g.dy) > 6,
986
+ onPanResponderGrant: () => {
987
+ isDragging.current = false;
988
+ posX.stopAnimation();
989
+ posY.stopAnimation();
990
+ },
991
+ onPanResponderMove: (_, g) => {
992
+ if (Math.abs(g.dx) > 6 || Math.abs(g.dy) > 6) {
993
+ isDragging.current = true;
994
+ scale.setValue(1);
995
+ }
996
+ const { width, height } = getScreenBounds();
997
+ const newX = clamp(
998
+ currentX.current + g.dx,
999
+ EDGE_PADDING,
1000
+ width - FAB_SIZE - EDGE_PADDING
1001
+ );
1002
+ const newY = clamp(
1003
+ currentY.current + g.dy,
1004
+ -(height - FAB_SIZE - baseBottom - 60),
1005
+ 0
1006
+ );
1007
+ posX.setValue(newX);
1008
+ posY.setValue(newY);
1009
+ },
1010
+ onPanResponderRelease: (_, g) => {
1011
+ if (isDragging.current) {
1012
+ snapAndPersist(currentX.current + g.dx, currentY.current + g.dy);
1013
+ }
1014
+ isDragging.current = false;
1015
+ }
1016
+ }) : PanResponder2.create({})
1017
+ ).current;
809
1018
  const handlePressIn = useCallback2(() => {
1019
+ if (isDragging.current) return;
810
1020
  Animated2.spring(scale, {
811
1021
  toValue: 0.85,
812
1022
  useNativeDriver: true,
@@ -821,39 +1031,35 @@ var MushiFloatingButton = ({
821
1031
  damping: 10,
822
1032
  stiffness: 250
823
1033
  }).start();
824
- }, [scale]);
825
- const positionStyle = position === "bottom-left" ? { left: inset?.left ?? 20 } : { right: inset?.right ?? 20 };
1034
+ if (!isDragging.current) onPress();
1035
+ }, [scale, onPress]);
826
1036
  return /* @__PURE__ */ jsx2(
827
- TouchableWithoutFeedback,
1037
+ Animated2.View,
828
1038
  {
829
- onPress,
830
- onPressIn: handlePressIn,
831
- onPressOut: handlePressOut,
832
- children: /* @__PURE__ */ jsx2(
833
- Animated2.View,
1039
+ ...draggable ? panResponder.panHandlers : {},
1040
+ style: [
1041
+ s2.fab,
834
1042
  {
835
- style: [
836
- s2.fab,
837
- positionStyle,
838
- { bottom: inset?.bottom ?? (Platform3.OS === "ios" ? 50 : 28) },
839
- {
840
- backgroundColor: dark ? "#0a84ff" : "#007aff",
841
- transform: [{ scale }],
842
- shadowColor: dark ? "#0a84ff" : "#000"
843
- }
844
- ],
845
- children: /* @__PURE__ */ jsx2(Animated2.Text, { style: s2.emoji, children: "\u{1F41B}" })
1043
+ left: posX,
1044
+ bottom: Animated2.add(new Animated2.Value(baseBottom), Animated2.multiply(posY, -1)),
1045
+ backgroundColor: accentColor,
1046
+ shadowColor: dark ? accentColor : "#000",
1047
+ transform: [{ scale }]
846
1048
  }
847
- )
1049
+ ],
1050
+ onStartShouldSetResponder: () => !draggable,
1051
+ onResponderGrant: handlePressIn,
1052
+ onResponderRelease: handlePressOut,
1053
+ children: /* @__PURE__ */ jsx2(Animated2.Text, { style: s2.emoji, children: MUSHI_COPY.triggerText })
848
1054
  }
849
1055
  );
850
1056
  };
851
1057
  var s2 = StyleSheet2.create({
852
1058
  fab: {
853
1059
  position: "absolute",
854
- width: 56,
855
- height: 56,
856
- borderRadius: 28,
1060
+ width: FAB_SIZE,
1061
+ height: FAB_SIZE,
1062
+ borderRadius: FAB_SIZE / 2,
857
1063
  alignItems: "center",
858
1064
  justifyContent: "center",
859
1065
  zIndex: 9999,
@@ -867,14 +1073,53 @@ var s2 = StyleSheet2.create({
867
1073
  }
868
1074
  });
869
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
+
870
1080
  // src/provider.tsx
871
1081
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
872
1082
  var MushiContext = createContext(null);
873
- 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
+ }
874
1115
  const consoleRef = useRef3(null);
875
1116
  const networkRef = useRef3(null);
876
1117
  const queueRef = useRef3(null);
877
1118
  const apiClientRef = useRef3(null);
1119
+ const sessionIdRef = useRef3(`sess_${newUuid()}`);
1120
+ const breadcrumbsRef = useRef3(
1121
+ createBreadcrumbBuffer({ max: config.capture?.maxBreadcrumbs ?? 50 })
1122
+ );
878
1123
  const [sheetVisible, setSheetVisible] = useState2(false);
879
1124
  const [sheetScreenshot, setSheetScreenshot] = useState2(null);
880
1125
  const reporterTokenRef = useRef3(`rn-${config.projectId}-anon`);
@@ -885,6 +1130,8 @@ function MushiProvider({ children, ...config }) {
885
1130
  resolveReporterTokenReady = resolve;
886
1131
  })
887
1132
  );
1133
+ const userTokenRef = useRef3(null);
1134
+ const pageContextRef = useRef3(null);
888
1135
  const rawEndpoint = config.apiEndpoint ?? config.endpoint;
889
1136
  if (rawEndpoint !== void 0 && rawEndpoint.trim() === "") {
890
1137
  throw new Error(
@@ -895,7 +1142,7 @@ function MushiProvider({ children, ...config }) {
895
1142
  console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
896
1143
  }
897
1144
  const apiEndpoint = rawEndpoint ?? DEFAULT_API_ENDPOINT;
898
- useEffect2(() => {
1145
+ useEffect3(() => {
899
1146
  if (config.capture?.console !== false) {
900
1147
  consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
901
1148
  }
@@ -905,7 +1152,8 @@ function MushiProvider({ children, ...config }) {
905
1152
  apiClientRef.current = createApiClient({
906
1153
  projectId: config.projectId,
907
1154
  apiKey: config.apiKey,
908
- apiEndpoint
1155
+ apiEndpoint,
1156
+ getUserToken: () => userTokenRef.current
909
1157
  });
910
1158
  queueRef.current = new AsyncStorageQueue({
911
1159
  maxSize: config.storage?.maxQueueSize,
@@ -918,6 +1166,12 @@ function MushiProvider({ children, ...config }) {
918
1166
  });
919
1167
  queueRef.current.flush().catch(() => {
920
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
+ });
921
1175
  (async () => {
922
1176
  const TOKEN_KEY = "@mushi:reporter_token";
923
1177
  try {
@@ -944,7 +1198,7 @@ function MushiProvider({ children, ...config }) {
944
1198
  networkRef.current?.restore();
945
1199
  };
946
1200
  }, []);
947
- useEffect2(() => {
1201
+ useEffect3(() => {
948
1202
  let unsubscribe;
949
1203
  try {
950
1204
  const NetInfo = __require("@react-native-community/netinfo").default;
@@ -959,6 +1213,17 @@ function MushiProvider({ children, ...config }) {
959
1213
  return () => unsubscribe?.();
960
1214
  }, []);
961
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
+ }
962
1227
  let vshot = null;
963
1228
  try {
964
1229
  vshot = __require("react-native-view-shot");
@@ -976,10 +1241,10 @@ function MushiProvider({ children, ...config }) {
976
1241
  }).finally(() => {
977
1242
  setSheetVisible(true);
978
1243
  });
979
- }, []);
1244
+ }, [config.capture?.screenshot]);
980
1245
  const close = useCallback3(() => setSheetVisible(false), []);
981
1246
  const attachTo = useCallback3(() => ({ onPress: open }), [open]);
982
- useEffect2(() => {
1247
+ useEffect3(() => {
983
1248
  const trigger2 = config.widget?.trigger ?? "button";
984
1249
  if (trigger2 !== "shake" && trigger2 !== "both") return;
985
1250
  let disposed = false;
@@ -1010,14 +1275,35 @@ function MushiProvider({ children, ...config }) {
1010
1275
  const submitReport = useCallback3(
1011
1276
  async (data) => {
1012
1277
  const deviceInfo = getDeviceInfo();
1278
+ breadcrumbsRef.current.add({
1279
+ category: "lifecycle",
1280
+ level: "info",
1281
+ message: "Report submitted",
1282
+ data: { category: data.category }
1283
+ });
1013
1284
  const contextMeta = {};
1014
- 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
+ }
1015
1294
  if (userRef.current?.email) contextMeta.userEmail = userRef.current.email;
1016
1295
  if (userRef.current?.name) contextMeta.userName = userRef.current.name;
1017
1296
  if (screenRef.current?.name) contextMeta.screenName = screenRef.current.name;
1018
1297
  if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
1019
1298
  if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
1020
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);
1021
1307
  const report = {
1022
1308
  id: newUuid(),
1023
1309
  projectId: config.projectId,
@@ -1040,7 +1326,14 @@ function MushiProvider({ children, ...config }) {
1040
1326
  })) ?? [],
1041
1327
  screenshotDataUrl: data.screenshotDataUrl,
1042
1328
  metadata: merged,
1329
+ sessionId: sessionIdRef.current,
1043
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 } : {},
1044
1337
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1045
1338
  };
1046
1339
  const client = apiClientRef.current;
@@ -1060,7 +1353,7 @@ function MushiProvider({ children, ...config }) {
1060
1353
  const screenRef = useRef3(null);
1061
1354
  const activityQueueRef = useRef3([]);
1062
1355
  const rewardsFlushRef = useRef3(null);
1063
- useEffect2(() => {
1356
+ useEffect3(() => {
1064
1357
  const rewards = config.rewards;
1065
1358
  if (!rewards?.enabled || !rewards.trackActivity) return;
1066
1359
  const flushMs = Math.max(3e4, rewards.flushIntervalMs ?? 3e5);
@@ -1098,11 +1391,53 @@ function MushiProvider({ children, ...config }) {
1098
1391
  provider: traits?.provider
1099
1392
  };
1100
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
+ },
1101
1418
  setMetadata(key, value) {
1102
1419
  metadataRef.current[key] = value;
1103
1420
  },
1104
1421
  setScreen(screen) {
1422
+ const prev = screenRef.current?.name;
1105
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);
1106
1441
  },
1107
1442
  // Reporter API — scoped to the device's persistent reporter token
1108
1443
  async listMyReports() {
@@ -1243,10 +1578,10 @@ function MushiTrigger({ children }) {
1243
1578
  }
1244
1579
 
1245
1580
  // src/capture/navigation-capture.ts
1246
- import { useEffect as useEffect3, useRef as useRef4 } from "react";
1581
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
1247
1582
  function useNavigationCapture(navigationRef) {
1248
1583
  const historyRef = useRef4([]);
1249
- useEffect3(() => {
1584
+ useEffect4(() => {
1250
1585
  if (!navigationRef?.addListener || !navigationRef?.getCurrentRoute) return;
1251
1586
  const unsubscribe = navigationRef.addListener("state", () => {
1252
1587
  const route = navigationRef.getCurrentRoute?.();