@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/CONTRIBUTING.md CHANGED
@@ -58,12 +58,12 @@ packages/
58
58
  react-native/ React Native SDK (MIT)
59
59
  cli/ CLI tool (MIT)
60
60
  mcp/ MCP server for coding agents (MIT)
61
- server/ Supabase Edge Functions (BSL)
62
- agents/ Agentic fix pipeline (BSL)
63
- verify/ Fix verification (BSL)
61
+ server/ Supabase Edge Functions (AGPLv3)
62
+ agents/ Agentic fix pipeline (AGPLv3)
63
+ verify/ Fix verification (AGPLv3)
64
64
  apps/
65
65
  admin/ Admin dashboard (React + Tailwind)
66
- docs/ Documentation site (planned)
66
+ docs/ Documentation site (Nextra — apps/docs)
67
67
  tooling/
68
68
  eslint-config/ Shared ESLint flat config
69
69
  tsconfig/ Shared TypeScript configs
@@ -81,6 +81,30 @@ tooling/
81
81
  ```
82
82
  6. Open a pull request
83
83
 
84
+ ### Documentation (docs-as-code)
85
+
86
+ If your PR changes **user-visible SDK or CLI behavior**, update docs in the **same PR**:
87
+
88
+ | Surface | When to update |
89
+ | --- | --- |
90
+ | `packages/*/README.md` | npm-facing install/API/config changes for that package |
91
+ | `apps/docs/content/**/*.mdx` | Long-form guides, quickstarts, admin console docs |
92
+ | Root `README.md` | Marketing stats only when counts change — run `pnpm check:docs-stats` |
93
+
94
+ Guardrails (run locally before pushing):
95
+
96
+ ```bash
97
+ pnpm check:docs-stats # root README stat badges
98
+ pnpm check:sdk-version-matrix # apps/docs/content/sdks/index.mdx versions
99
+ pnpm check:internal-doc-links # dead /foo links in MDX
100
+ pnpm check:onboarding-drift # phantom MCP env vars / stale tool names
101
+ pnpm check:bundle-docs # @mushi-mushi/web size-limit vs README
102
+ pnpm gen:llms-txt # regenerate apps/docs/public/llms.txt after nav changes
103
+ pnpm gen:mcp-tools-doc # regenerate sdks/mcp-tools.generated.mdx from catalog.ts
104
+ ```
105
+
106
+ README changes on npm packages only appear on [npmjs.com](https://www.npmjs.com) after the next **Changesets publish**.
107
+
84
108
  ## Changesets
85
109
 
86
110
  We use [Changesets](https://github.com/changesets/changesets) for versioning. If your PR modifies a published package (`core`, `web`, `react`, `vue`, `svelte`, `angular`, `react-native`, `cli`, `mcp`), add a changeset:
@@ -93,13 +117,30 @@ Select the affected packages, the semver bump type, and write a summary. The cha
93
117
 
94
118
  ## Release flow
95
119
 
96
- Releases are fully automated. Maintainers don't run `npm publish` by hand.
120
+ Releases are fully automated once the version PR is merged. Maintainers don't run
121
+ `npm publish` by hand for routine bumps. See **[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md)**
122
+ for the full production runbook (edge functions, admin SPA, docs site, migrations).
97
123
 
98
124
  1. PRs land on `master` with one or more changeset files in `.changeset/`.
99
- 2. `release.yml` runs on every push to `master`. It opens (or updates) a `chore: version packages` PR that bumps every affected `package.json`, rolls up the changelogs, and deletes the consumed changesets.
100
- 3. Merging that "Version Packages" PR re-fires `release.yml`. The publish step authenticates to npm via **OpenID Connect (OIDC) Trusted Publishers** — no long-lived `NPM_TOKEN` is exchanged — and every tarball ships with a **Sigstore provenance attestation** uploaded to the public transparency log.
125
+ Orphaned changesets (only ignored packages) fail `pnpm check:changeset-orphans`.
126
+ 2. `release.yml` runs on every push to `master`. It opens (or updates) a
127
+ **`chore: version packages`** PR on `changeset-release/master` that bumps
128
+ affected `package.json` files, rolls up changelogs, and deletes consumed
129
+ changesets.
130
+ 3. **Wait for CI on the version PR.** If checks don't appear (bot-branch
131
+ suppression), push an empty commit to `changeset-release/master`. If
132
+ `check:sdk-version-matrix` fails, run `pnpm gen:sdk-version-matrix` on that
133
+ branch and commit `apps/docs/content/sdks/index.mdx`.
134
+ 4. Merge the version PR. The publish step uses **OIDC Trusted Publishers** +
135
+ Sigstore provenance (with `NPM_TOKEN` only for brand-new package bootstrap).
136
+ 5. **Manually dispatch Release** unless a run starts within ~2 minutes:
137
+ **Actions → Release → Run workflow → `master`**. Squash-merges attributed to
138
+ `github-actions[bot]` usually suppress the auto `push` trigger — manual
139
+ dispatch is the normal path, not a fallback of last resort.
101
140
 
102
- If GitHub's anti-loop protection suppresses the auto re-fire (the squash merge can be attributed to `github-actions[bot]`), trigger the workflow manually: **Actions → release → Run workflow → master**.
141
+ ```bash
142
+ gh workflow run release.yml --ref master
143
+ ```
103
144
 
104
145
  ### Adding a brand-new publishable package
105
146
 
@@ -146,7 +187,7 @@ chore: bump dependencies
146
187
  ## License
147
188
 
148
189
  - SDK packages are MIT — your contributions will be MIT-licensed
149
- - Server/agents/verify are BSL 1.1 — contributions to those packages fall under BSL
190
+ - Server/agents/verify are AGPLv3 — contributions to those packages fall under AGPLv3
150
191
 
151
192
  ## Questions?
152
193
 
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # @mushi-mushi/react-native
2
2
 
3
+ > **Your AI wrote it. Mushi tells you why it broke.**
4
+ > Plain-English diagnosis + a paste-ready fix, right inside Cursor. MIT-licensed SDKs · self-hostable · no second LLM key.
5
+
3
6
  React Native SDK for [Mushi Mushi](https://www.npmjs.com/package/mushi-mushi) — shake-to-report widget, offline queue, and the closed-loop lesson layer for iOS + Android.
4
7
 
5
8
  > **One-command setup:** `npx mushi-mushi` auto-detects React Native / Expo and installs this package with the right env vars.
@@ -91,27 +94,59 @@ import { MushiBottomSheet, MushiFloatingButton } from '@mushi-mushi/react-native
91
94
 
92
95
  ## What gets captured
93
96
 
94
- - Device info (platform, OS version, screen dimensions)
97
+ - Device info (platform, OS version, screen dimensions) + a stable device `fingerprintHash`
98
+ - A per-launch `sessionId` so every report from the same run is grouped
99
+ - `sdkPackage` / `sdkVersion` / `appVersion` so the console can flag outdated installs
95
100
  - Console logs (monkey-patched `console.*`)
96
101
  - Network requests (fetch interceptor, headers redacted)
97
102
  - Navigation events (React Navigation integration, optional)
103
+ - A 50-entry breadcrumb ring buffer → sent as `breadcrumbs` + a derived repro `timeline` (SDK lifecycle, screen changes via `setScreen`, and host crumbs via `addBreadcrumb`)
104
+ - Screenshot of the current screen (optional — see below)
98
105
  - Offline queue via `@react-native-async-storage/async-storage` — reports survive force-close
99
106
 
107
+ ## Screenshots
108
+
109
+ When `react-native-view-shot` is installed, the SDK captures the current screen **before** the report sheet overlays it, and shows the user a thumbnail they can remove before submitting. Enabled by default; turn off with `capture: { screenshot: false }`.
110
+
111
+ ```bash
112
+ npx expo install react-native-view-shot # v4+ on Expo SDK 55; v5+ for Fabric / New Architecture
113
+ ```
114
+
115
+ > **Masking sensitive screens.** `captureScreen()` grabs whatever is on screen, which on a finance/health app can include balances or PII. The user always reviews and can remove the thumbnail before submit, but for screens that should never be captured, set `capture: { screenshot: false }` while they're focused, or use `expo-screen-capture` to mark the view secure. Field-level masking is on the roadmap.
116
+
117
+ ## Identifying the reporter
118
+
119
+ Call `identify()` / `setUser()` whenever auth state changes so reports show a human name instead of an anonymous token in the console:
120
+
121
+ ```tsx
122
+ const mushi = useMushi()
123
+ mushi.identify(user.id, { email: user.email, name: user.displayName, provider: 'supabase' })
124
+ // or, web-SDK style:
125
+ mushi.setUser({ id: user.id, email: user.email, name: user.displayName, provider: 'supabase' })
126
+ ```
127
+
128
+ Mushi stores only a SHA-256 hash of the email (the host app owns the raw PII) and shows the `name` as the reporter's display name.
129
+
100
130
  ## Peer dependencies
101
131
 
102
132
  - `react` >= 18
103
133
  - `react-native` >= 0.72
104
134
  - `@react-navigation/native` >= 6 (optional — navigation capture)
105
135
  - `@react-native-async-storage/async-storage` >= 1.19 (optional — offline queue)
136
+ - `react-native-view-shot` >= 3.8 (optional — screenshot capture; v5+ for New Architecture / Fabric)
106
137
  - `expo-sensors` (optional — shake trigger only)
107
138
 
108
139
  ## Changelog highlights
109
140
 
141
+ - **v0.17.0** — Report payload parity with the web SDK: nested `metadata.user` identity (fixes the anonymous-token reporter on the server), `setUser()` alias, per-launch `sessionId`, `sdkPackage`/`sdkVersion`/`appVersion`, device `fingerprintHash`, and a breadcrumb ring buffer → `breadcrumbs` + repro `timeline`. `capture.screenshot` toggle + `addBreadcrumb()`. Bottom sheet restyled to match the web widget.
110
142
  - **v0.11.0** — Hermes compatibility fix: replaced `new Function()` dynamic imports with lazy `require()` for `@react-native-community/netinfo` and `expo-sensors`. Enables `trigger: 'button'` safely on all RN 0.84+ Hermes/AOT builds. Adds `MushiTrigger` headless component.
111
143
  - **v0.8.x** — Shake-to-report listener, offline queue, navigation capture.
112
144
 
113
145
  See the full [CHANGELOG](https://github.com/kensaurus/mushi-mushi/blob/master/packages/react-native/CHANGELOG.md).
114
146
 
147
+ Maintainers: report ingest contract and empty-state semantics are documented in
148
+ [`docs/report-ingest-contract.md`](../../docs/report-ingest-contract.md).
149
+
115
150
  ## License
116
151
 
117
152
  MIT
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,7 +498,8 @@ 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)", 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" };
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,
@@ -492,6 +524,10 @@ var MushiBottomSheet = ({
492
524
  ],
493
525
  children: [
494
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 }] }) }),
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
+ ] }),
495
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)(
496
532
  import_react_native2.TouchableOpacity,
497
533
  {
@@ -501,7 +537,7 @@ var MushiBottomSheet = ({
501
537
  else setSelectedReportId(null);
502
538
  },
503
539
  style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
504
- 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" : tab === "inbox" ? "Inbox" : "\u{1F310}" })
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" })
505
541
  },
506
542
  tab
507
543
  )) }),
@@ -514,7 +550,7 @@ var MushiBottomSheet = ({
514
550
  VERIFIABLE_STATUSES.has(
515
551
  inboxReports.find((r) => r.id === selectedReportId)?.status ?? ""
516
552
  ) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.verifyRow, children: [
517
- /* @__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" }) }),
518
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" }) })
519
555
  ] }),
520
556
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -527,12 +563,11 @@ var MushiBottomSheet = ({
527
563
  onChangeText: setReplyText
528
564
  }
529
565
  ),
530
- /* @__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" }) })
531
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: [
532
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) }),
533
569
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.sub, fontSize: 11 }, children: r.status })
534
570
  ] }, r.id)) }) : sheetTab === "community" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: [
535
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.title, { color: colors.text }], children: "\u{1F310} Mushi Community" }),
536
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." }),
537
572
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
538
573
  import_react_native2.TextInput,
@@ -546,7 +581,7 @@ var MushiBottomSheet = ({
546
581
  textContentType: "emailAddress"
547
582
  }
548
583
  ),
549
- /* @__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, children: "Send sign-in link \u2192" }) }),
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" }) }),
550
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." })
551
586
  ] }) : phase === "sent" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.sentWrap, children: [
552
587
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.sentEmoji], children: "\u2705" }),
@@ -558,7 +593,7 @@ var MushiBottomSheet = ({
558
593
  keyboardShouldPersistTaps: "handled",
559
594
  contentContainerStyle: { paddingBottom: 24 },
560
595
  children: [
561
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.title, { color: colors.text }], children: "Report an issue" }),
596
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.stepLabel, { color: colors.sub }], children: "What kind of issue?" }),
562
597
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.catRow, children: CATEGORIES.map((c) => {
563
598
  const active = category === c.key;
564
599
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
@@ -580,7 +615,7 @@ var MushiBottomSheet = ({
580
615
  {
581
616
  style: [
582
617
  s.catLabel,
583
- { color: active ? "#fff" : colors.text }
618
+ { color: active ? colors.accentInk : colors.text }
584
619
  ],
585
620
  children: c.label
586
621
  }
@@ -646,7 +681,7 @@ var MushiBottomSheet = ({
646
681
  s.submitBtn,
647
682
  { backgroundColor: canSubmit ? colors.accent : colors.disabled }
648
683
  ],
649
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.submitText, !canSubmit && { color: colors.disabledText }], children: phase === "sending" ? "Sending\u2026" : "Submit" })
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" })
650
685
  }
651
686
  )
652
687
  ]
@@ -662,7 +697,13 @@ var MushiBottomSheet = ({
662
697
  var s = import_react_native2.StyleSheet.create({
663
698
  flex: { flex: 1 },
664
699
  backdrop: {
665
- ...import_react_native2.StyleSheet.absoluteFillObject
700
+ // Inlined StyleSheet.absoluteFillObject — RN 0.86 dropped it from the
701
+ // StyleSheet TS types; the literal is equivalent and works on all RN >=0.72.
702
+ position: "absolute",
703
+ top: 0,
704
+ left: 0,
705
+ right: 0,
706
+ bottom: 0
666
707
  },
667
708
  sheet: {
668
709
  position: "absolute",
@@ -689,6 +730,24 @@ var s = import_react_native2.StyleSheet.create({
689
730
  borderRadius: 3,
690
731
  opacity: 0.5
691
732
  },
733
+ brandHeader: {
734
+ paddingHorizontal: 20,
735
+ paddingTop: 10,
736
+ paddingBottom: 12,
737
+ borderBottomWidth: 1.5
738
+ },
739
+ brandEyebrow: {
740
+ fontSize: 10,
741
+ fontWeight: "700",
742
+ letterSpacing: 1.2,
743
+ marginBottom: 2,
744
+ opacity: 0.7
745
+ },
746
+ brandTitle: {
747
+ fontSize: 18,
748
+ fontWeight: "800",
749
+ letterSpacing: -0.2
750
+ },
692
751
  body: {
693
752
  paddingHorizontal: 20,
694
753
  paddingTop: 4
@@ -698,6 +757,14 @@ var s = import_react_native2.StyleSheet.create({
698
757
  fontWeight: "700",
699
758
  marginBottom: 16
700
759
  },
760
+ stepLabel: {
761
+ fontSize: 12,
762
+ fontWeight: "600",
763
+ letterSpacing: 0.2,
764
+ marginTop: 8,
765
+ marginBottom: 10,
766
+ textTransform: "uppercase"
767
+ },
701
768
  catRow: {
702
769
  flexDirection: "row",
703
770
  justifyContent: "space-between",
@@ -826,6 +893,7 @@ var s = import_react_native2.StyleSheet.create({
826
893
  // src/components/MushiFloatingButton.tsx
827
894
  var import_react2 = require("react");
828
895
  var import_react_native3 = require("react-native");
896
+ var import_core2 = require("@mushi-mushi/core");
829
897
  var import_jsx_runtime2 = require("react/jsx-runtime");
830
898
  var FAB_SIZE = 56;
831
899
  var EDGE_PADDING = 12;
@@ -877,7 +945,7 @@ var MushiFloatingButton = ({
877
945
  const currentX = (0, import_react2.useRef)(baseX);
878
946
  const currentY = (0, import_react2.useRef)(0);
879
947
  const isDragging = (0, import_react2.useRef)(false);
880
- const accentColor = accent ?? (dark ? "#0a84ff" : "#007aff");
948
+ const accentColor = accent ?? (0, import_core2.mushiPalette)(dark ? "dark" : "light").accent;
881
949
  (0, import_react2.useEffect)(() => {
882
950
  if (!persist) return;
883
951
  void loadStoredPos().then((pos) => {
@@ -987,7 +1055,7 @@ var MushiFloatingButton = ({
987
1055
  onStartShouldSetResponder: () => !draggable,
988
1056
  onResponderGrant: handlePressIn,
989
1057
  onResponderRelease: handlePressOut,
990
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children: "\u{1F41B}" })
1058
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children: import_core2.MUSHI_COPY.triggerText })
991
1059
  }
992
1060
  );
993
1061
  };
@@ -1010,14 +1078,53 @@ var s2 = import_react_native3.StyleSheet.create({
1010
1078
  }
1011
1079
  });
1012
1080
 
1081
+ // src/version.ts
1082
+ var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
1083
+ var MUSHI_SDK_VERSION = "0.18.1" ? "0.18.1" : "0.17.0";
1084
+
1013
1085
  // src/provider.tsx
1014
1086
  var import_jsx_runtime3 = require("react/jsx-runtime");
1015
1087
  var MushiContext = (0, import_react3.createContext)(null);
1016
- function MushiProvider({ children, ...config }) {
1088
+ function breadcrumbToTimelineKind(category) {
1089
+ switch (category) {
1090
+ case "navigation":
1091
+ return "route";
1092
+ case "ui.click":
1093
+ case "ui.tap":
1094
+ return "click";
1095
+ case "xhr":
1096
+ case "fetch":
1097
+ case "network":
1098
+ return "request";
1099
+ default:
1100
+ return "log";
1101
+ }
1102
+ }
1103
+ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1104
+ const proc = globalThis.process;
1105
+ const envProjectId = proc?.env?.MUSHI_PROJECT_ID || proc?.env?.EXPO_PUBLIC_MUSHI_PROJECT_ID;
1106
+ const envApiKey = proc?.env?.MUSHI_API_KEY || proc?.env?.EXPO_PUBLIC_MUSHI_API_KEY;
1107
+ const config = {
1108
+ ...envProjectId ? { projectId: envProjectId } : {},
1109
+ ...envApiKey ? { apiKey: envApiKey } : {},
1110
+ ...configProp,
1111
+ ...barePropConfig
1112
+ };
1113
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
1114
+ if (!config.projectId || !config.apiKey) {
1115
+ console.warn(
1116
+ "[MushiProvider] Missing projectId or apiKey \u2014 set MUSHI_PROJECT_ID / MUSHI_API_KEY (or EXPO_PUBLIC_* equivalents) or pass them as props."
1117
+ );
1118
+ }
1119
+ }
1017
1120
  const consoleRef = (0, import_react3.useRef)(null);
1018
1121
  const networkRef = (0, import_react3.useRef)(null);
1019
1122
  const queueRef = (0, import_react3.useRef)(null);
1020
1123
  const apiClientRef = (0, import_react3.useRef)(null);
1124
+ const sessionIdRef = (0, import_react3.useRef)(`sess_${(0, import_core3.newUuid)()}`);
1125
+ const breadcrumbsRef = (0, import_react3.useRef)(
1126
+ (0, import_core3.createBreadcrumbBuffer)({ max: config.capture?.maxBreadcrumbs ?? 50 })
1127
+ );
1021
1128
  const [sheetVisible, setSheetVisible] = (0, import_react3.useState)(false);
1022
1129
  const [sheetScreenshot, setSheetScreenshot] = (0, import_react3.useState)(null);
1023
1130
  const reporterTokenRef = (0, import_react3.useRef)(`rn-${config.projectId}-anon`);
@@ -1028,6 +1135,8 @@ function MushiProvider({ children, ...config }) {
1028
1135
  resolveReporterTokenReady = resolve;
1029
1136
  })
1030
1137
  );
1138
+ const userTokenRef = (0, import_react3.useRef)(null);
1139
+ const pageContextRef = (0, import_react3.useRef)(null);
1031
1140
  const rawEndpoint = config.apiEndpoint ?? config.endpoint;
1032
1141
  if (rawEndpoint !== void 0 && rawEndpoint.trim() === "") {
1033
1142
  throw new Error(
@@ -1037,7 +1146,7 @@ function MushiProvider({ children, ...config }) {
1037
1146
  if (config.endpoint && !config.apiEndpoint && __DEV__) {
1038
1147
  console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
1039
1148
  }
1040
- const apiEndpoint = rawEndpoint ?? import_core.DEFAULT_API_ENDPOINT;
1149
+ const apiEndpoint = rawEndpoint ?? import_core3.DEFAULT_API_ENDPOINT;
1041
1150
  (0, import_react3.useEffect)(() => {
1042
1151
  if (config.capture?.console !== false) {
1043
1152
  consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
@@ -1045,10 +1154,13 @@ function MushiProvider({ children, ...config }) {
1045
1154
  if (config.capture?.network !== false) {
1046
1155
  networkRef.current = setupNetworkCapture(config.capture?.maxNetworkEntries, apiEndpoint);
1047
1156
  }
1048
- apiClientRef.current = (0, import_core.createApiClient)({
1157
+ apiClientRef.current = (0, import_core3.createApiClient)({
1049
1158
  projectId: config.projectId,
1050
1159
  apiKey: config.apiKey,
1051
- apiEndpoint
1160
+ apiEndpoint,
1161
+ getUserToken: () => userTokenRef.current,
1162
+ sdkPackage: MUSHI_SDK_PACKAGE,
1163
+ sdkVersion: MUSHI_SDK_VERSION
1052
1164
  });
1053
1165
  queueRef.current = new AsyncStorageQueue({
1054
1166
  maxSize: config.storage?.maxQueueSize,
@@ -1061,6 +1173,12 @@ function MushiProvider({ children, ...config }) {
1061
1173
  });
1062
1174
  queueRef.current.flush().catch(() => {
1063
1175
  });
1176
+ breadcrumbsRef.current.add({
1177
+ category: "lifecycle",
1178
+ level: "info",
1179
+ message: "Mushi SDK initialised",
1180
+ data: { sdk: MUSHI_SDK_PACKAGE, version: MUSHI_SDK_VERSION }
1181
+ });
1064
1182
  (async () => {
1065
1183
  const TOKEN_KEY = "@mushi:reporter_token";
1066
1184
  try {
@@ -1069,14 +1187,14 @@ function MushiProvider({ children, ...config }) {
1069
1187
  if (existing && existing.startsWith("mushi_")) {
1070
1188
  reporterTokenRef.current = existing;
1071
1189
  } else {
1072
- const fresh = `mushi_${(0, import_core.newUuid)()}`;
1190
+ const fresh = `mushi_${(0, import_core3.newUuid)()}`;
1073
1191
  reporterTokenRef.current = fresh;
1074
1192
  await AsyncStorage.setItem(TOKEN_KEY, fresh);
1075
1193
  }
1076
1194
  resolveReporterTokenReady();
1077
1195
  } catch {
1078
1196
  if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
1079
- reporterTokenRef.current = `mushi_${(0, import_core.newUuid)()}`;
1197
+ reporterTokenRef.current = `mushi_${(0, import_core3.newUuid)()}`;
1080
1198
  }
1081
1199
  resolveReporterTokenReady();
1082
1200
  }
@@ -1102,6 +1220,17 @@ function MushiProvider({ children, ...config }) {
1102
1220
  return () => unsubscribe?.();
1103
1221
  }, []);
1104
1222
  const open = (0, import_react3.useCallback)(() => {
1223
+ breadcrumbsRef.current.add({
1224
+ category: "lifecycle",
1225
+ level: "info",
1226
+ message: "Widget opened",
1227
+ ...screenRef.current?.name ? { data: { screen: screenRef.current.name } } : {}
1228
+ });
1229
+ if (config.capture?.screenshot === false) {
1230
+ setSheetScreenshot(null);
1231
+ setSheetVisible(true);
1232
+ return;
1233
+ }
1105
1234
  let vshot = null;
1106
1235
  try {
1107
1236
  vshot = require("react-native-view-shot");
@@ -1119,7 +1248,7 @@ function MushiProvider({ children, ...config }) {
1119
1248
  }).finally(() => {
1120
1249
  setSheetVisible(true);
1121
1250
  });
1122
- }, []);
1251
+ }, [config.capture?.screenshot]);
1123
1252
  const close = (0, import_react3.useCallback)(() => setSheetVisible(false), []);
1124
1253
  const attachTo = (0, import_react3.useCallback)(() => ({ onPress: open }), [open]);
1125
1254
  (0, import_react3.useEffect)(() => {
@@ -1153,16 +1282,37 @@ function MushiProvider({ children, ...config }) {
1153
1282
  const submitReport = (0, import_react3.useCallback)(
1154
1283
  async (data) => {
1155
1284
  const deviceInfo = getDeviceInfo();
1285
+ breadcrumbsRef.current.add({
1286
+ category: "lifecycle",
1287
+ level: "info",
1288
+ message: "Report submitted",
1289
+ data: { category: data.category }
1290
+ });
1156
1291
  const contextMeta = {};
1157
- if (userRef.current?.id) contextMeta.userId = userRef.current.id;
1292
+ if (userRef.current?.id) {
1293
+ contextMeta.user = {
1294
+ id: userRef.current.id,
1295
+ ...userRef.current.email ? { email: userRef.current.email } : {},
1296
+ ...userRef.current.name ? { name: userRef.current.name } : {},
1297
+ ...userRef.current.provider ? { provider: userRef.current.provider } : {}
1298
+ };
1299
+ contextMeta.userId = userRef.current.id;
1300
+ }
1158
1301
  if (userRef.current?.email) contextMeta.userEmail = userRef.current.email;
1159
1302
  if (userRef.current?.name) contextMeta.userName = userRef.current.name;
1160
1303
  if (screenRef.current?.name) contextMeta.screenName = screenRef.current.name;
1161
1304
  if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
1162
1305
  if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
1163
1306
  const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
1307
+ const breadcrumbs = breadcrumbsRef.current.getAll();
1308
+ const timeline = breadcrumbs.map((c) => ({
1309
+ ts: c.timestamp,
1310
+ kind: breadcrumbToTimelineKind(c.category),
1311
+ payload: { message: c.message, level: c.level, ...c.data ?? {} }
1312
+ }));
1313
+ const fingerprintHash = getDeviceFingerprintHash(deviceInfo);
1164
1314
  const report = {
1165
- id: (0, import_core.newUuid)(),
1315
+ id: (0, import_core3.newUuid)(),
1166
1316
  projectId: config.projectId,
1167
1317
  category: data.category,
1168
1318
  description: data.description,
@@ -1183,7 +1333,14 @@ function MushiProvider({ children, ...config }) {
1183
1333
  })) ?? [],
1184
1334
  screenshotDataUrl: data.screenshotDataUrl,
1185
1335
  metadata: merged,
1336
+ sessionId: sessionIdRef.current,
1186
1337
  reporterToken: reporterTokenRef.current,
1338
+ ...fingerprintHash ? { fingerprintHash } : {},
1339
+ ...deviceInfo.appVersion ? { appVersion: deviceInfo.appVersion } : {},
1340
+ sdkPackage: MUSHI_SDK_PACKAGE,
1341
+ sdkVersion: MUSHI_SDK_VERSION,
1342
+ ...breadcrumbs.length > 0 ? { breadcrumbs } : {},
1343
+ ...timeline.length > 0 ? { timeline } : {},
1187
1344
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1188
1345
  };
1189
1346
  const client = apiClientRef.current;
@@ -1241,11 +1398,53 @@ function MushiProvider({ children, ...config }) {
1241
1398
  provider: traits?.provider
1242
1399
  };
1243
1400
  },
1401
+ setUser(user) {
1402
+ userRef.current = {
1403
+ id: user.id,
1404
+ email: user.email,
1405
+ name: user.name,
1406
+ provider: user.provider
1407
+ };
1408
+ },
1409
+ identifyWithToken(token) {
1410
+ userTokenRef.current = token && typeof token === "string" ? token : null;
1411
+ if (userTokenRef.current) {
1412
+ const claims = (0, import_core3.parseIdentityToken)(userTokenRef.current);
1413
+ if (claims?.sub) {
1414
+ userRef.current = {
1415
+ id: claims.sub,
1416
+ ...claims.email ? { email: claims.email } : {},
1417
+ ...claims.name ? { name: claims.name } : {}
1418
+ };
1419
+ }
1420
+ }
1421
+ },
1422
+ publishPageContext(context) {
1423
+ pageContextRef.current = context && context.route ? context : null;
1424
+ },
1244
1425
  setMetadata(key, value) {
1245
1426
  metadataRef.current[key] = value;
1246
1427
  },
1247
1428
  setScreen(screen) {
1429
+ const prev = screenRef.current?.name;
1248
1430
  screenRef.current = screen;
1431
+ if (screen.name && screen.name !== prev) {
1432
+ breadcrumbsRef.current.add({
1433
+ category: "navigation",
1434
+ level: "info",
1435
+ message: `Navigated to ${screen.name}`,
1436
+ // Always include `route` (fallback to the screen name) so the
1437
+ // admin repro-timeline renders the destination instead of
1438
+ // "unknown route" when the host only set a screen name.
1439
+ data: {
1440
+ route: screen.route ?? screen.name,
1441
+ ...screen.feature ? { feature: screen.feature } : {}
1442
+ }
1443
+ });
1444
+ }
1445
+ },
1446
+ addBreadcrumb(crumb) {
1447
+ breadcrumbsRef.current.add(crumb);
1249
1448
  },
1250
1449
  // Reporter API — scoped to the device's persistent reporter token
1251
1450
  async listMyReports() {