@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.cjs CHANGED
@@ -47,7 +47,7 @@ module.exports = __toCommonJS(index_exports);
47
47
 
48
48
  // src/provider.tsx
49
49
  var import_react3 = require("react");
50
- var import_core = require("@mushi-mushi/core");
50
+ var import_core3 = require("@mushi-mushi/core");
51
51
 
52
52
  // src/capture/console-capture.ts
53
53
  function setupConsoleCapture(maxEntries = 100) {
@@ -164,6 +164,30 @@ function getDeviceInfo() {
164
164
  };
165
165
  }
166
166
 
167
+ // src/capture/fingerprint.ts
168
+ function nonCryptoHashHex(input) {
169
+ let hash = 0;
170
+ for (let i = 0; i < input.length; i++) {
171
+ hash = (hash << 5) - hash + input.charCodeAt(i);
172
+ hash |= 0;
173
+ }
174
+ return `rnfp_${(hash >>> 0).toString(16).padStart(8, "0")}`;
175
+ }
176
+ function getDeviceFingerprintHash(device) {
177
+ const inputs = {
178
+ platform: device.platform,
179
+ osVersion: String(device.osVersion ?? ""),
180
+ screenWidth: device.screenWidth,
181
+ screenHeight: device.screenHeight,
182
+ pixelRatio: device.pixelRatio,
183
+ locale: device.locale ?? "",
184
+ timezone: device.timezone ?? ""
185
+ };
186
+ const serialised = JSON.stringify(inputs);
187
+ if (!device.platform && !device.screenWidth) return null;
188
+ return nonCryptoHashHex(serialised);
189
+ }
190
+
167
191
  // src/storage/async-storage-queue.ts
168
192
  var QUEUE_KEY = "@mushi:offline_queue";
169
193
  var AsyncStorageQueue = class {
@@ -317,10 +341,17 @@ function sleep(ms) {
317
341
  // src/components/MushiBottomSheet.tsx
318
342
  var import_react = require("react");
319
343
  var import_react_native2 = require("react-native");
344
+ var import_core = require("@mushi-mushi/core");
320
345
  var import_jsx_runtime = require("react/jsx-runtime");
321
346
  var { height: SCREEN_HEIGHT } = import_react_native2.Dimensions.get("window");
322
347
  var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
323
348
  var DISMISS_THRESHOLD = 80;
349
+ var NEON = {
350
+ accent: "#0FFF50",
351
+ accentHover: "#00E646",
352
+ ink: "#0a1a0a",
353
+ border: "#00C43A"
354
+ };
324
355
  var CATEGORIES = [
325
356
  { key: "bug", emoji: "\u{1F41B}", label: "Bug" },
326
357
  { key: "slow", emoji: "\u{1F422}", label: "Slow" },
@@ -467,13 +498,15 @@ var MushiBottomSheet = ({
467
498
  }
468
499
  };
469
500
  const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
470
- 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)" };
501
+ const pal = (0, import_core.mushiPalette)(dark ? "dark" : "light");
502
+ 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 };
471
503
  const canSubmit = !!category && description.trim().length > 0 && phase === "form";
472
504
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
473
505
  import_react_native2.KeyboardAvoidingView,
474
506
  {
475
507
  style: s.flex,
476
- behavior: import_react_native2.Platform.OS === "ios" ? "padding" : void 0,
508
+ behavior: import_react_native2.Platform.OS === "ios" ? "padding" : "height",
509
+ keyboardVerticalOffset: import_react_native2.Platform.OS === "ios" ? 0 : 20,
477
510
  children: [
478
511
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
479
512
  import_react_native2.Animated.View,
@@ -491,7 +524,11 @@ var MushiBottomSheet = ({
491
524
  ],
492
525
  children: [
493
526
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
494
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.tabRow, children: ["report", "inbox"].map((tab) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
527
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: [s.brandHeader, { backgroundColor: NEON.accent, borderBottomColor: NEON.border }], children: [
528
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.brandEyebrow, { color: NEON.ink }], children: "MUSHI \xB7 BETA" }),
529
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.brandTitle, { color: NEON.ink }], children: sheetTab === "inbox" ? "Your reports" : sheetTab === "community" ? "Community" : "Report an issue" })
530
+ ] }),
531
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.tabRow, children: ["report", "inbox", "community"].map((tab) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
495
532
  import_react_native2.TouchableOpacity,
496
533
  {
497
534
  onPress: () => {
@@ -500,11 +537,11 @@ var MushiBottomSheet = ({
500
537
  else setSelectedReportId(null);
501
538
  },
502
539
  style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
503
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.accent : colors.sub }], children: tab === "report" ? "Report" : "Inbox" })
540
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.text : colors.sub }], children: tab === "report" ? "Report" : tab === "inbox" ? "Your reports" : "Community" })
504
541
  },
505
542
  tab
506
543
  )) }),
507
- sheetTab === "inbox" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.body, children: inboxLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub }, children: "Loading\u2026" }) : selectedReportId ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
544
+ sheetTab === "inbox" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: inboxLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub }, children: "Loading\u2026" }) : selectedReportId ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
508
545
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { onPress: () => setSelectedReportId(null), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.accent, marginBottom: 8 }, children: "\u2190 Back" }) }),
509
546
  threadComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: [s.threadBubble, { backgroundColor: colors.card }], children: [
510
547
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, fontSize: 11 }, children: c.author_kind }),
@@ -513,7 +550,7 @@ var MushiBottomSheet = ({
513
550
  VERIFIABLE_STATUSES.has(
514
551
  inboxReports.find((r) => r.id === selectedReportId)?.status ?? ""
515
552
  ) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.verifyRow, children: [
516
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: s.submitText, children: "Yes, fixed" }) }),
553
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.accentInk }], children: "Yes, fixed" }) }),
517
554
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.border }], onPress: () => submitFeedback("not_fixed"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.text }], children: "Not fixed" }) })
518
555
  ] }),
519
556
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -526,106 +563,130 @@ var MushiBottomSheet = ({
526
563
  onChangeText: setReplyText
527
564
  }
528
565
  ),
529
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: s.submitText, children: "Send" }) })
566
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.accentInk }], children: "Send" }) })
530
567
  ] }) : inboxReports.map((r) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
531
568
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.text, fontWeight: "600" }, numberOfLines: 1, children: (r.summary ?? r.description ?? "Report").slice(0, 60) }),
532
569
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, fontSize: 11 }, children: r.status })
533
- ] }, r.id)) }) : phase === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.sentWrap, children: [
570
+ ] }, r.id)) }) : sheetTab === "community" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: [
571
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.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." }),
572
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
573
+ import_react_native2.TextInput,
574
+ {
575
+ style: [s.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border, minHeight: 48 }],
576
+ placeholder: "you@example.com",
577
+ placeholderTextColor: colors.sub,
578
+ keyboardType: "email-address",
579
+ autoCapitalize: "none",
580
+ autoCorrect: false,
581
+ textContentType: "emailAddress"
582
+ }
583
+ ),
584
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: colors.accentInk }], children: "Send sign-in link \u2192" }) }),
585
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.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." })
586
+ ] }) : phase === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.sentWrap, children: [
534
587
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentEmoji], children: "\u2705" }),
535
588
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentText, { color: colors.text }], children: "Report sent!" })
536
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.body, children: [
537
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.title, { color: colors.text }], children: "Report an issue" }),
538
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.catRow, children: CATEGORIES.map((c) => {
539
- const active = category === c.key;
540
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
541
- import_react_native2.TouchableOpacity,
542
- {
543
- onPress: () => setCategory(c.key),
544
- activeOpacity: 0.7,
545
- style: [
546
- s.catBtn,
589
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
590
+ import_react_native2.ScrollView,
591
+ {
592
+ style: s.body,
593
+ keyboardShouldPersistTaps: "handled",
594
+ contentContainerStyle: { paddingBottom: 24 },
595
+ children: [
596
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.stepLabel, { color: colors.sub }], children: "What kind of issue?" }),
597
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.catRow, children: CATEGORIES.map((c) => {
598
+ const active = category === c.key;
599
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
600
+ import_react_native2.TouchableOpacity,
547
601
  {
548
- backgroundColor: active ? colors.accent : colors.card,
549
- borderColor: active ? colors.accent : colors.border
550
- }
551
- ],
552
- children: [
553
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: s.catEmoji, children: c.emoji }),
554
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
555
- import_react_native2.Text,
602
+ onPress: () => setCategory(c.key),
603
+ activeOpacity: 0.7,
604
+ style: [
605
+ s.catBtn,
606
+ {
607
+ backgroundColor: active ? colors.accent : colors.card,
608
+ borderColor: active ? colors.accent : colors.border
609
+ }
610
+ ],
611
+ children: [
612
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: s.catEmoji, children: c.emoji }),
613
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
614
+ import_react_native2.Text,
615
+ {
616
+ style: [
617
+ s.catLabel,
618
+ { color: active ? colors.accentInk : colors.text }
619
+ ],
620
+ children: c.label
621
+ }
622
+ )
623
+ ]
624
+ },
625
+ c.key
626
+ );
627
+ }) }),
628
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
629
+ import_react_native2.TextInput,
630
+ {
631
+ style: [
632
+ s.input,
556
633
  {
557
- style: [
558
- s.catLabel,
559
- { color: active ? "#fff" : colors.text }
560
- ],
561
- children: c.label
634
+ backgroundColor: colors.card,
635
+ color: colors.text,
636
+ borderColor: colors.border
562
637
  }
563
- )
564
- ]
565
- },
566
- c.key
567
- );
568
- }) }),
569
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
570
- import_react_native2.TextInput,
571
- {
572
- style: [
573
- s.input,
638
+ ],
639
+ placeholder: "What happened?",
640
+ placeholderTextColor: colors.sub,
641
+ multiline: true,
642
+ textAlignVertical: "top",
643
+ value: description,
644
+ onChangeText: setDescription,
645
+ editable: phase === "form"
646
+ }
647
+ ),
648
+ activeScreenshot ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotRow, children: [
649
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
650
+ import_react_native2.Image,
651
+ {
652
+ source: { uri: activeScreenshot },
653
+ style: s.screenshotThumb,
654
+ accessibilityLabel: "Attached screenshot"
655
+ }
656
+ ),
657
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotMeta, children: [
658
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotLabel, { color: colors.text }], children: "Screenshot attached" }),
659
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotSub, { color: colors.sub }], children: "Helps the team see the issue" })
660
+ ] }),
661
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
662
+ import_react_native2.TouchableOpacity,
663
+ {
664
+ onPress: () => {
665
+ setScreenshotAttached(false);
666
+ onClearScreenshot?.();
667
+ },
668
+ hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
669
+ accessibilityLabel: "Remove screenshot",
670
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
671
+ }
672
+ )
673
+ ] }) : null,
674
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
675
+ import_react_native2.TouchableOpacity,
574
676
  {
575
- backgroundColor: colors.card,
576
- color: colors.text,
577
- borderColor: colors.border
677
+ onPress: handleSubmit,
678
+ disabled: !canSubmit,
679
+ activeOpacity: 0.8,
680
+ style: [
681
+ s.submitBtn,
682
+ { backgroundColor: canSubmit ? colors.accent : colors.disabled }
683
+ ],
684
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, { color: canSubmit ? colors.accentInk : colors.disabledText }], children: phase === "sending" ? "Sending\u2026" : "Submit report" })
578
685
  }
579
- ],
580
- placeholder: "What happened?",
581
- placeholderTextColor: colors.sub,
582
- multiline: true,
583
- textAlignVertical: "top",
584
- value: description,
585
- onChangeText: setDescription,
586
- editable: phase === "form"
587
- }
588
- ),
589
- activeScreenshot ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotRow, children: [
590
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
591
- import_react_native2.Image,
592
- {
593
- source: { uri: activeScreenshot },
594
- style: s.screenshotThumb,
595
- accessibilityLabel: "Attached screenshot"
596
- }
597
- ),
598
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotMeta, children: [
599
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotLabel, { color: colors.text }], children: "Screenshot attached" }),
600
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotSub, { color: colors.sub }], children: "Helps the team see the issue" })
601
- ] }),
602
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
603
- import_react_native2.TouchableOpacity,
604
- {
605
- onPress: () => {
606
- setScreenshotAttached(false);
607
- onClearScreenshot?.();
608
- },
609
- hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
610
- accessibilityLabel: "Remove screenshot",
611
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
612
- }
613
- )
614
- ] }) : null,
615
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
616
- import_react_native2.TouchableOpacity,
617
- {
618
- onPress: handleSubmit,
619
- disabled: !canSubmit,
620
- activeOpacity: 0.8,
621
- style: [
622
- s.submitBtn,
623
- { backgroundColor: canSubmit ? colors.accent : colors.border }
624
- ],
625
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: s.submitText, children: phase === "sending" ? "Sending\u2026" : "Submit" })
626
- }
627
- )
628
- ] })
686
+ )
687
+ ]
688
+ }
689
+ )
629
690
  ]
630
691
  }
631
692
  )
@@ -663,6 +724,24 @@ var s = import_react_native2.StyleSheet.create({
663
724
  borderRadius: 3,
664
725
  opacity: 0.5
665
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
+ },
666
745
  body: {
667
746
  paddingHorizontal: 20,
668
747
  paddingTop: 4
@@ -672,6 +751,14 @@ var s = import_react_native2.StyleSheet.create({
672
751
  fontWeight: "700",
673
752
  marginBottom: 16
674
753
  },
754
+ stepLabel: {
755
+ fontSize: 12,
756
+ fontWeight: "600",
757
+ letterSpacing: 0.2,
758
+ marginTop: 8,
759
+ marginBottom: 10,
760
+ textTransform: "uppercase"
761
+ },
675
762
  catRow: {
676
763
  flexDirection: "row",
677
764
  justifyContent: "space-between",
@@ -800,16 +887,135 @@ var s = import_react_native2.StyleSheet.create({
800
887
  // src/components/MushiFloatingButton.tsx
801
888
  var import_react2 = require("react");
802
889
  var import_react_native3 = require("react-native");
890
+ var import_core2 = require("@mushi-mushi/core");
803
891
  var import_jsx_runtime2 = require("react/jsx-runtime");
892
+ var FAB_SIZE = 56;
893
+ var EDGE_PADDING = 12;
894
+ function getScreenBounds() {
895
+ const { width, height } = import_react_native3.Dimensions.get("window");
896
+ return { width, height };
897
+ }
898
+ function clamp(value, min, max) {
899
+ return Math.min(Math.max(value, min), max);
900
+ }
901
+ var STORAGE_KEY = "mushi:fab-pos";
902
+ async function loadStoredPos() {
903
+ try {
904
+ const AsyncStorage = (await import("@react-native-async-storage/async-storage")).default;
905
+ const raw = await AsyncStorage.getItem(STORAGE_KEY);
906
+ if (!raw) return null;
907
+ return JSON.parse(raw);
908
+ } catch {
909
+ return null;
910
+ }
911
+ }
912
+ async function storePos(pos) {
913
+ try {
914
+ const AsyncStorage = (await import("@react-native-async-storage/async-storage")).default;
915
+ await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(pos));
916
+ } catch {
917
+ }
918
+ }
804
919
  var MushiFloatingButton = ({
805
920
  onPress,
806
921
  position = "bottom-right",
807
- inset
922
+ inset,
923
+ config,
924
+ accent
808
925
  }) => {
809
926
  const scheme = (0, import_react_native3.useColorScheme)();
810
927
  const dark = scheme === "dark";
928
+ const draggable = config?.draggable ?? false;
929
+ const snapToEdge = config?.snapToEdge ?? true;
930
+ const persist = config?.persist ?? false;
931
+ const baseBottom = inset?.bottom ?? (import_react_native3.Platform.OS === "ios" ? 50 : 28);
932
+ const baseX = position === "bottom-left" ? inset?.left ?? EDGE_PADDING : (() => {
933
+ const { width } = getScreenBounds();
934
+ return width - FAB_SIZE - (inset?.right ?? EDGE_PADDING);
935
+ })();
811
936
  const scale = (0, import_react2.useRef)(new import_react_native3.Animated.Value(1)).current;
937
+ const posX = (0, import_react2.useRef)(new import_react_native3.Animated.Value(baseX)).current;
938
+ const posY = (0, import_react2.useRef)(new import_react_native3.Animated.Value(0)).current;
939
+ const currentX = (0, import_react2.useRef)(baseX);
940
+ const currentY = (0, import_react2.useRef)(0);
941
+ const isDragging = (0, import_react2.useRef)(false);
942
+ const accentColor = accent ?? (0, import_core2.mushiPalette)(dark ? "dark" : "light").accent;
943
+ (0, import_react2.useEffect)(() => {
944
+ if (!persist) return;
945
+ void loadStoredPos().then((pos) => {
946
+ if (!pos) return;
947
+ currentX.current = pos.x;
948
+ currentY.current = pos.y;
949
+ posX.setValue(pos.x);
950
+ posY.setValue(pos.y);
951
+ });
952
+ }, [persist, posX, posY]);
953
+ const snapAndPersist = (0, import_react2.useCallback)(
954
+ (x, y) => {
955
+ const { width, height } = getScreenBounds();
956
+ let snappedX = x;
957
+ if (snapToEdge) {
958
+ const midX = width / 2;
959
+ snappedX = x + FAB_SIZE / 2 < midX ? EDGE_PADDING : width - FAB_SIZE - EDGE_PADDING;
960
+ }
961
+ const clampedX = clamp(snappedX, EDGE_PADDING, width - FAB_SIZE - EDGE_PADDING);
962
+ const clampedY = clamp(y, -(height - FAB_SIZE - baseBottom - 60), 0);
963
+ currentX.current = clampedX;
964
+ currentY.current = clampedY;
965
+ import_react_native3.Animated.spring(posX, {
966
+ toValue: clampedX,
967
+ useNativeDriver: false,
968
+ damping: 18,
969
+ stiffness: 220
970
+ }).start();
971
+ import_react_native3.Animated.spring(posY, {
972
+ toValue: clampedY,
973
+ useNativeDriver: false,
974
+ damping: 18,
975
+ stiffness: 220
976
+ }).start();
977
+ if (persist) void storePos({ x: clampedX, y: clampedY });
978
+ },
979
+ [snapToEdge, persist, posX, posY, baseBottom]
980
+ );
981
+ const panResponder = (0, import_react2.useRef)(
982
+ draggable ? import_react_native3.PanResponder.create({
983
+ onStartShouldSetPanResponder: () => true,
984
+ onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 6 || Math.abs(g.dy) > 6,
985
+ onPanResponderGrant: () => {
986
+ isDragging.current = false;
987
+ posX.stopAnimation();
988
+ posY.stopAnimation();
989
+ },
990
+ onPanResponderMove: (_, g) => {
991
+ if (Math.abs(g.dx) > 6 || Math.abs(g.dy) > 6) {
992
+ isDragging.current = true;
993
+ scale.setValue(1);
994
+ }
995
+ const { width, height } = getScreenBounds();
996
+ const newX = clamp(
997
+ currentX.current + g.dx,
998
+ EDGE_PADDING,
999
+ width - FAB_SIZE - EDGE_PADDING
1000
+ );
1001
+ const newY = clamp(
1002
+ currentY.current + g.dy,
1003
+ -(height - FAB_SIZE - baseBottom - 60),
1004
+ 0
1005
+ );
1006
+ posX.setValue(newX);
1007
+ posY.setValue(newY);
1008
+ },
1009
+ onPanResponderRelease: (_, g) => {
1010
+ if (isDragging.current) {
1011
+ snapAndPersist(currentX.current + g.dx, currentY.current + g.dy);
1012
+ }
1013
+ isDragging.current = false;
1014
+ }
1015
+ }) : import_react_native3.PanResponder.create({})
1016
+ ).current;
812
1017
  const handlePressIn = (0, import_react2.useCallback)(() => {
1018
+ if (isDragging.current) return;
813
1019
  import_react_native3.Animated.spring(scale, {
814
1020
  toValue: 0.85,
815
1021
  useNativeDriver: true,
@@ -824,39 +1030,35 @@ var MushiFloatingButton = ({
824
1030
  damping: 10,
825
1031
  stiffness: 250
826
1032
  }).start();
827
- }, [scale]);
828
- const positionStyle = position === "bottom-left" ? { left: inset?.left ?? 20 } : { right: inset?.right ?? 20 };
1033
+ if (!isDragging.current) onPress();
1034
+ }, [scale, onPress]);
829
1035
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
830
- import_react_native3.TouchableWithoutFeedback,
1036
+ import_react_native3.Animated.View,
831
1037
  {
832
- onPress,
833
- onPressIn: handlePressIn,
834
- onPressOut: handlePressOut,
835
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
836
- import_react_native3.Animated.View,
1038
+ ...draggable ? panResponder.panHandlers : {},
1039
+ style: [
1040
+ s2.fab,
837
1041
  {
838
- style: [
839
- s2.fab,
840
- positionStyle,
841
- { bottom: inset?.bottom ?? (import_react_native3.Platform.OS === "ios" ? 50 : 28) },
842
- {
843
- backgroundColor: dark ? "#0a84ff" : "#007aff",
844
- transform: [{ scale }],
845
- shadowColor: dark ? "#0a84ff" : "#000"
846
- }
847
- ],
848
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children: "\u{1F41B}" })
1042
+ left: posX,
1043
+ bottom: import_react_native3.Animated.add(new import_react_native3.Animated.Value(baseBottom), import_react_native3.Animated.multiply(posY, -1)),
1044
+ backgroundColor: accentColor,
1045
+ shadowColor: dark ? accentColor : "#000",
1046
+ transform: [{ scale }]
849
1047
  }
850
- )
1048
+ ],
1049
+ onStartShouldSetResponder: () => !draggable,
1050
+ onResponderGrant: handlePressIn,
1051
+ onResponderRelease: handlePressOut,
1052
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children: import_core2.MUSHI_COPY.triggerText })
851
1053
  }
852
1054
  );
853
1055
  };
854
1056
  var s2 = import_react_native3.StyleSheet.create({
855
1057
  fab: {
856
1058
  position: "absolute",
857
- width: 56,
858
- height: 56,
859
- borderRadius: 28,
1059
+ width: FAB_SIZE,
1060
+ height: FAB_SIZE,
1061
+ borderRadius: FAB_SIZE / 2,
860
1062
  alignItems: "center",
861
1063
  justifyContent: "center",
862
1064
  zIndex: 9999,
@@ -870,14 +1072,53 @@ var s2 = import_react_native3.StyleSheet.create({
870
1072
  }
871
1073
  });
872
1074
 
1075
+ // src/version.ts
1076
+ var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
1077
+ var MUSHI_SDK_VERSION = "0.17.0" ? "0.17.0" : "0.17.0";
1078
+
873
1079
  // src/provider.tsx
874
1080
  var import_jsx_runtime3 = require("react/jsx-runtime");
875
1081
  var MushiContext = (0, import_react3.createContext)(null);
876
- function MushiProvider({ children, ...config }) {
1082
+ function breadcrumbToTimelineKind(category) {
1083
+ switch (category) {
1084
+ case "navigation":
1085
+ return "route";
1086
+ case "ui.click":
1087
+ case "ui.tap":
1088
+ return "click";
1089
+ case "xhr":
1090
+ case "fetch":
1091
+ case "network":
1092
+ return "request";
1093
+ default:
1094
+ return "log";
1095
+ }
1096
+ }
1097
+ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1098
+ const proc = globalThis.process;
1099
+ const envProjectId = proc?.env?.MUSHI_PROJECT_ID || proc?.env?.EXPO_PUBLIC_MUSHI_PROJECT_ID;
1100
+ const envApiKey = proc?.env?.MUSHI_API_KEY || proc?.env?.EXPO_PUBLIC_MUSHI_API_KEY;
1101
+ const config = {
1102
+ ...envProjectId ? { projectId: envProjectId } : {},
1103
+ ...envApiKey ? { apiKey: envApiKey } : {},
1104
+ ...configProp,
1105
+ ...barePropConfig
1106
+ };
1107
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
1108
+ if (!config.projectId || !config.apiKey) {
1109
+ console.warn(
1110
+ "[MushiProvider] Missing projectId or apiKey \u2014 set MUSHI_PROJECT_ID / MUSHI_API_KEY (or EXPO_PUBLIC_* equivalents) or pass them as props."
1111
+ );
1112
+ }
1113
+ }
877
1114
  const consoleRef = (0, import_react3.useRef)(null);
878
1115
  const networkRef = (0, import_react3.useRef)(null);
879
1116
  const queueRef = (0, import_react3.useRef)(null);
880
1117
  const apiClientRef = (0, import_react3.useRef)(null);
1118
+ const sessionIdRef = (0, import_react3.useRef)(`sess_${(0, import_core3.newUuid)()}`);
1119
+ const breadcrumbsRef = (0, import_react3.useRef)(
1120
+ (0, import_core3.createBreadcrumbBuffer)({ max: config.capture?.maxBreadcrumbs ?? 50 })
1121
+ );
881
1122
  const [sheetVisible, setSheetVisible] = (0, import_react3.useState)(false);
882
1123
  const [sheetScreenshot, setSheetScreenshot] = (0, import_react3.useState)(null);
883
1124
  const reporterTokenRef = (0, import_react3.useRef)(`rn-${config.projectId}-anon`);
@@ -888,6 +1129,8 @@ function MushiProvider({ children, ...config }) {
888
1129
  resolveReporterTokenReady = resolve;
889
1130
  })
890
1131
  );
1132
+ const userTokenRef = (0, import_react3.useRef)(null);
1133
+ const pageContextRef = (0, import_react3.useRef)(null);
891
1134
  const rawEndpoint = config.apiEndpoint ?? config.endpoint;
892
1135
  if (rawEndpoint !== void 0 && rawEndpoint.trim() === "") {
893
1136
  throw new Error(
@@ -897,7 +1140,7 @@ function MushiProvider({ children, ...config }) {
897
1140
  if (config.endpoint && !config.apiEndpoint && __DEV__) {
898
1141
  console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
899
1142
  }
900
- const apiEndpoint = rawEndpoint ?? import_core.DEFAULT_API_ENDPOINT;
1143
+ const apiEndpoint = rawEndpoint ?? import_core3.DEFAULT_API_ENDPOINT;
901
1144
  (0, import_react3.useEffect)(() => {
902
1145
  if (config.capture?.console !== false) {
903
1146
  consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
@@ -905,10 +1148,11 @@ function MushiProvider({ children, ...config }) {
905
1148
  if (config.capture?.network !== false) {
906
1149
  networkRef.current = setupNetworkCapture(config.capture?.maxNetworkEntries, apiEndpoint);
907
1150
  }
908
- apiClientRef.current = (0, import_core.createApiClient)({
1151
+ apiClientRef.current = (0, import_core3.createApiClient)({
909
1152
  projectId: config.projectId,
910
1153
  apiKey: config.apiKey,
911
- apiEndpoint
1154
+ apiEndpoint,
1155
+ getUserToken: () => userTokenRef.current
912
1156
  });
913
1157
  queueRef.current = new AsyncStorageQueue({
914
1158
  maxSize: config.storage?.maxQueueSize,
@@ -921,6 +1165,12 @@ function MushiProvider({ children, ...config }) {
921
1165
  });
922
1166
  queueRef.current.flush().catch(() => {
923
1167
  });
1168
+ breadcrumbsRef.current.add({
1169
+ category: "lifecycle",
1170
+ level: "info",
1171
+ message: "Mushi SDK initialised",
1172
+ data: { sdk: MUSHI_SDK_PACKAGE, version: MUSHI_SDK_VERSION }
1173
+ });
924
1174
  (async () => {
925
1175
  const TOKEN_KEY = "@mushi:reporter_token";
926
1176
  try {
@@ -929,14 +1179,14 @@ function MushiProvider({ children, ...config }) {
929
1179
  if (existing && existing.startsWith("mushi_")) {
930
1180
  reporterTokenRef.current = existing;
931
1181
  } else {
932
- const fresh = `mushi_${(0, import_core.newUuid)()}`;
1182
+ const fresh = `mushi_${(0, import_core3.newUuid)()}`;
933
1183
  reporterTokenRef.current = fresh;
934
1184
  await AsyncStorage.setItem(TOKEN_KEY, fresh);
935
1185
  }
936
1186
  resolveReporterTokenReady();
937
1187
  } catch {
938
1188
  if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
939
- reporterTokenRef.current = `mushi_${(0, import_core.newUuid)()}`;
1189
+ reporterTokenRef.current = `mushi_${(0, import_core3.newUuid)()}`;
940
1190
  }
941
1191
  resolveReporterTokenReady();
942
1192
  }
@@ -962,6 +1212,17 @@ function MushiProvider({ children, ...config }) {
962
1212
  return () => unsubscribe?.();
963
1213
  }, []);
964
1214
  const open = (0, import_react3.useCallback)(() => {
1215
+ breadcrumbsRef.current.add({
1216
+ category: "lifecycle",
1217
+ level: "info",
1218
+ message: "Widget opened",
1219
+ ...screenRef.current?.name ? { data: { screen: screenRef.current.name } } : {}
1220
+ });
1221
+ if (config.capture?.screenshot === false) {
1222
+ setSheetScreenshot(null);
1223
+ setSheetVisible(true);
1224
+ return;
1225
+ }
965
1226
  let vshot = null;
966
1227
  try {
967
1228
  vshot = require("react-native-view-shot");
@@ -979,7 +1240,7 @@ function MushiProvider({ children, ...config }) {
979
1240
  }).finally(() => {
980
1241
  setSheetVisible(true);
981
1242
  });
982
- }, []);
1243
+ }, [config.capture?.screenshot]);
983
1244
  const close = (0, import_react3.useCallback)(() => setSheetVisible(false), []);
984
1245
  const attachTo = (0, import_react3.useCallback)(() => ({ onPress: open }), [open]);
985
1246
  (0, import_react3.useEffect)(() => {
@@ -1013,16 +1274,37 @@ function MushiProvider({ children, ...config }) {
1013
1274
  const submitReport = (0, import_react3.useCallback)(
1014
1275
  async (data) => {
1015
1276
  const deviceInfo = getDeviceInfo();
1277
+ breadcrumbsRef.current.add({
1278
+ category: "lifecycle",
1279
+ level: "info",
1280
+ message: "Report submitted",
1281
+ data: { category: data.category }
1282
+ });
1016
1283
  const contextMeta = {};
1017
- if (userRef.current?.id) contextMeta.userId = userRef.current.id;
1284
+ if (userRef.current?.id) {
1285
+ contextMeta.user = {
1286
+ id: userRef.current.id,
1287
+ ...userRef.current.email ? { email: userRef.current.email } : {},
1288
+ ...userRef.current.name ? { name: userRef.current.name } : {},
1289
+ ...userRef.current.provider ? { provider: userRef.current.provider } : {}
1290
+ };
1291
+ contextMeta.userId = userRef.current.id;
1292
+ }
1018
1293
  if (userRef.current?.email) contextMeta.userEmail = userRef.current.email;
1019
1294
  if (userRef.current?.name) contextMeta.userName = userRef.current.name;
1020
1295
  if (screenRef.current?.name) contextMeta.screenName = screenRef.current.name;
1021
1296
  if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
1022
1297
  if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
1023
1298
  const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
1299
+ const breadcrumbs = breadcrumbsRef.current.getAll();
1300
+ const timeline = breadcrumbs.map((c) => ({
1301
+ ts: c.timestamp,
1302
+ kind: breadcrumbToTimelineKind(c.category),
1303
+ payload: { message: c.message, level: c.level, ...c.data ?? {} }
1304
+ }));
1305
+ const fingerprintHash = getDeviceFingerprintHash(deviceInfo);
1024
1306
  const report = {
1025
- id: (0, import_core.newUuid)(),
1307
+ id: (0, import_core3.newUuid)(),
1026
1308
  projectId: config.projectId,
1027
1309
  category: data.category,
1028
1310
  description: data.description,
@@ -1043,7 +1325,14 @@ function MushiProvider({ children, ...config }) {
1043
1325
  })) ?? [],
1044
1326
  screenshotDataUrl: data.screenshotDataUrl,
1045
1327
  metadata: merged,
1328
+ sessionId: sessionIdRef.current,
1046
1329
  reporterToken: reporterTokenRef.current,
1330
+ ...fingerprintHash ? { fingerprintHash } : {},
1331
+ ...deviceInfo.appVersion ? { appVersion: deviceInfo.appVersion } : {},
1332
+ sdkPackage: MUSHI_SDK_PACKAGE,
1333
+ sdkVersion: MUSHI_SDK_VERSION,
1334
+ ...breadcrumbs.length > 0 ? { breadcrumbs } : {},
1335
+ ...timeline.length > 0 ? { timeline } : {},
1047
1336
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1048
1337
  };
1049
1338
  const client = apiClientRef.current;
@@ -1101,11 +1390,53 @@ function MushiProvider({ children, ...config }) {
1101
1390
  provider: traits?.provider
1102
1391
  };
1103
1392
  },
1393
+ setUser(user) {
1394
+ userRef.current = {
1395
+ id: user.id,
1396
+ email: user.email,
1397
+ name: user.name,
1398
+ provider: user.provider
1399
+ };
1400
+ },
1401
+ identifyWithToken(token) {
1402
+ userTokenRef.current = token && typeof token === "string" ? token : null;
1403
+ if (userTokenRef.current) {
1404
+ const claims = (0, import_core3.parseIdentityToken)(userTokenRef.current);
1405
+ if (claims?.sub) {
1406
+ userRef.current = {
1407
+ id: claims.sub,
1408
+ ...claims.email ? { email: claims.email } : {},
1409
+ ...claims.name ? { name: claims.name } : {}
1410
+ };
1411
+ }
1412
+ }
1413
+ },
1414
+ publishPageContext(context) {
1415
+ pageContextRef.current = context && context.route ? context : null;
1416
+ },
1104
1417
  setMetadata(key, value) {
1105
1418
  metadataRef.current[key] = value;
1106
1419
  },
1107
1420
  setScreen(screen) {
1421
+ const prev = screenRef.current?.name;
1108
1422
  screenRef.current = screen;
1423
+ if (screen.name && screen.name !== prev) {
1424
+ breadcrumbsRef.current.add({
1425
+ category: "navigation",
1426
+ level: "info",
1427
+ message: `Navigated to ${screen.name}`,
1428
+ // Always include `route` (fallback to the screen name) so the
1429
+ // admin repro-timeline renders the destination instead of
1430
+ // "unknown route" when the host only set a screen name.
1431
+ data: {
1432
+ route: screen.route ?? screen.name,
1433
+ ...screen.feature ? { feature: screen.feature } : {}
1434
+ }
1435
+ });
1436
+ }
1437
+ },
1438
+ addBreadcrumb(crumb) {
1439
+ breadcrumbsRef.current.add(crumb);
1109
1440
  },
1110
1441
  // Reporter API — scoped to the device's persistent reporter token
1111
1442
  async listMyReports() {