@mushi-mushi/react-native 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +932 -0
- package/CONTRIBUTING.md +50 -9
- package/README.md +36 -1
- package/dist/index.cjs +212 -21
- package/dist/index.d.cts +39 -2
- package/dist/index.d.ts +39 -2
- package/dist/index.js +209 -16
- package/package.json +4 -3
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 (
|
|
62
|
-
agents/ Agentic fix pipeline (
|
|
63
|
-
verify/ Fix verification (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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 ?
|
|
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,
|
|
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
|
]
|
|
@@ -689,6 +724,24 @@ var s = import_react_native2.StyleSheet.create({
|
|
|
689
724
|
borderRadius: 3,
|
|
690
725
|
opacity: 0.5
|
|
691
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
|
+
},
|
|
692
745
|
body: {
|
|
693
746
|
paddingHorizontal: 20,
|
|
694
747
|
paddingTop: 4
|
|
@@ -698,6 +751,14 @@ var s = import_react_native2.StyleSheet.create({
|
|
|
698
751
|
fontWeight: "700",
|
|
699
752
|
marginBottom: 16
|
|
700
753
|
},
|
|
754
|
+
stepLabel: {
|
|
755
|
+
fontSize: 12,
|
|
756
|
+
fontWeight: "600",
|
|
757
|
+
letterSpacing: 0.2,
|
|
758
|
+
marginTop: 8,
|
|
759
|
+
marginBottom: 10,
|
|
760
|
+
textTransform: "uppercase"
|
|
761
|
+
},
|
|
701
762
|
catRow: {
|
|
702
763
|
flexDirection: "row",
|
|
703
764
|
justifyContent: "space-between",
|
|
@@ -826,6 +887,7 @@ var s = import_react_native2.StyleSheet.create({
|
|
|
826
887
|
// src/components/MushiFloatingButton.tsx
|
|
827
888
|
var import_react2 = require("react");
|
|
828
889
|
var import_react_native3 = require("react-native");
|
|
890
|
+
var import_core2 = require("@mushi-mushi/core");
|
|
829
891
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
830
892
|
var FAB_SIZE = 56;
|
|
831
893
|
var EDGE_PADDING = 12;
|
|
@@ -877,7 +939,7 @@ var MushiFloatingButton = ({
|
|
|
877
939
|
const currentX = (0, import_react2.useRef)(baseX);
|
|
878
940
|
const currentY = (0, import_react2.useRef)(0);
|
|
879
941
|
const isDragging = (0, import_react2.useRef)(false);
|
|
880
|
-
const accentColor = accent ?? (dark ? "
|
|
942
|
+
const accentColor = accent ?? (0, import_core2.mushiPalette)(dark ? "dark" : "light").accent;
|
|
881
943
|
(0, import_react2.useEffect)(() => {
|
|
882
944
|
if (!persist) return;
|
|
883
945
|
void loadStoredPos().then((pos) => {
|
|
@@ -987,7 +1049,7 @@ var MushiFloatingButton = ({
|
|
|
987
1049
|
onStartShouldSetResponder: () => !draggable,
|
|
988
1050
|
onResponderGrant: handlePressIn,
|
|
989
1051
|
onResponderRelease: handlePressOut,
|
|
990
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children:
|
|
1052
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children: import_core2.MUSHI_COPY.triggerText })
|
|
991
1053
|
}
|
|
992
1054
|
);
|
|
993
1055
|
};
|
|
@@ -1010,14 +1072,53 @@ var s2 = import_react_native3.StyleSheet.create({
|
|
|
1010
1072
|
}
|
|
1011
1073
|
});
|
|
1012
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
|
+
|
|
1013
1079
|
// src/provider.tsx
|
|
1014
1080
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1015
1081
|
var MushiContext = (0, import_react3.createContext)(null);
|
|
1016
|
-
function
|
|
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
|
+
}
|
|
1017
1114
|
const consoleRef = (0, import_react3.useRef)(null);
|
|
1018
1115
|
const networkRef = (0, import_react3.useRef)(null);
|
|
1019
1116
|
const queueRef = (0, import_react3.useRef)(null);
|
|
1020
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
|
+
);
|
|
1021
1122
|
const [sheetVisible, setSheetVisible] = (0, import_react3.useState)(false);
|
|
1022
1123
|
const [sheetScreenshot, setSheetScreenshot] = (0, import_react3.useState)(null);
|
|
1023
1124
|
const reporterTokenRef = (0, import_react3.useRef)(`rn-${config.projectId}-anon`);
|
|
@@ -1028,6 +1129,8 @@ function MushiProvider({ children, ...config }) {
|
|
|
1028
1129
|
resolveReporterTokenReady = resolve;
|
|
1029
1130
|
})
|
|
1030
1131
|
);
|
|
1132
|
+
const userTokenRef = (0, import_react3.useRef)(null);
|
|
1133
|
+
const pageContextRef = (0, import_react3.useRef)(null);
|
|
1031
1134
|
const rawEndpoint = config.apiEndpoint ?? config.endpoint;
|
|
1032
1135
|
if (rawEndpoint !== void 0 && rawEndpoint.trim() === "") {
|
|
1033
1136
|
throw new Error(
|
|
@@ -1037,7 +1140,7 @@ function MushiProvider({ children, ...config }) {
|
|
|
1037
1140
|
if (config.endpoint && !config.apiEndpoint && __DEV__) {
|
|
1038
1141
|
console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
|
|
1039
1142
|
}
|
|
1040
|
-
const apiEndpoint = rawEndpoint ??
|
|
1143
|
+
const apiEndpoint = rawEndpoint ?? import_core3.DEFAULT_API_ENDPOINT;
|
|
1041
1144
|
(0, import_react3.useEffect)(() => {
|
|
1042
1145
|
if (config.capture?.console !== false) {
|
|
1043
1146
|
consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
|
|
@@ -1045,10 +1148,11 @@ function MushiProvider({ children, ...config }) {
|
|
|
1045
1148
|
if (config.capture?.network !== false) {
|
|
1046
1149
|
networkRef.current = setupNetworkCapture(config.capture?.maxNetworkEntries, apiEndpoint);
|
|
1047
1150
|
}
|
|
1048
|
-
apiClientRef.current = (0,
|
|
1151
|
+
apiClientRef.current = (0, import_core3.createApiClient)({
|
|
1049
1152
|
projectId: config.projectId,
|
|
1050
1153
|
apiKey: config.apiKey,
|
|
1051
|
-
apiEndpoint
|
|
1154
|
+
apiEndpoint,
|
|
1155
|
+
getUserToken: () => userTokenRef.current
|
|
1052
1156
|
});
|
|
1053
1157
|
queueRef.current = new AsyncStorageQueue({
|
|
1054
1158
|
maxSize: config.storage?.maxQueueSize,
|
|
@@ -1061,6 +1165,12 @@ function MushiProvider({ children, ...config }) {
|
|
|
1061
1165
|
});
|
|
1062
1166
|
queueRef.current.flush().catch(() => {
|
|
1063
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
|
+
});
|
|
1064
1174
|
(async () => {
|
|
1065
1175
|
const TOKEN_KEY = "@mushi:reporter_token";
|
|
1066
1176
|
try {
|
|
@@ -1069,14 +1179,14 @@ function MushiProvider({ children, ...config }) {
|
|
|
1069
1179
|
if (existing && existing.startsWith("mushi_")) {
|
|
1070
1180
|
reporterTokenRef.current = existing;
|
|
1071
1181
|
} else {
|
|
1072
|
-
const fresh = `mushi_${(0,
|
|
1182
|
+
const fresh = `mushi_${(0, import_core3.newUuid)()}`;
|
|
1073
1183
|
reporterTokenRef.current = fresh;
|
|
1074
1184
|
await AsyncStorage.setItem(TOKEN_KEY, fresh);
|
|
1075
1185
|
}
|
|
1076
1186
|
resolveReporterTokenReady();
|
|
1077
1187
|
} catch {
|
|
1078
1188
|
if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
|
|
1079
|
-
reporterTokenRef.current = `mushi_${(0,
|
|
1189
|
+
reporterTokenRef.current = `mushi_${(0, import_core3.newUuid)()}`;
|
|
1080
1190
|
}
|
|
1081
1191
|
resolveReporterTokenReady();
|
|
1082
1192
|
}
|
|
@@ -1102,6 +1212,17 @@ function MushiProvider({ children, ...config }) {
|
|
|
1102
1212
|
return () => unsubscribe?.();
|
|
1103
1213
|
}, []);
|
|
1104
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
|
+
}
|
|
1105
1226
|
let vshot = null;
|
|
1106
1227
|
try {
|
|
1107
1228
|
vshot = require("react-native-view-shot");
|
|
@@ -1119,7 +1240,7 @@ function MushiProvider({ children, ...config }) {
|
|
|
1119
1240
|
}).finally(() => {
|
|
1120
1241
|
setSheetVisible(true);
|
|
1121
1242
|
});
|
|
1122
|
-
}, []);
|
|
1243
|
+
}, [config.capture?.screenshot]);
|
|
1123
1244
|
const close = (0, import_react3.useCallback)(() => setSheetVisible(false), []);
|
|
1124
1245
|
const attachTo = (0, import_react3.useCallback)(() => ({ onPress: open }), [open]);
|
|
1125
1246
|
(0, import_react3.useEffect)(() => {
|
|
@@ -1153,16 +1274,37 @@ function MushiProvider({ children, ...config }) {
|
|
|
1153
1274
|
const submitReport = (0, import_react3.useCallback)(
|
|
1154
1275
|
async (data) => {
|
|
1155
1276
|
const deviceInfo = getDeviceInfo();
|
|
1277
|
+
breadcrumbsRef.current.add({
|
|
1278
|
+
category: "lifecycle",
|
|
1279
|
+
level: "info",
|
|
1280
|
+
message: "Report submitted",
|
|
1281
|
+
data: { category: data.category }
|
|
1282
|
+
});
|
|
1156
1283
|
const contextMeta = {};
|
|
1157
|
-
if (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
|
+
}
|
|
1158
1293
|
if (userRef.current?.email) contextMeta.userEmail = userRef.current.email;
|
|
1159
1294
|
if (userRef.current?.name) contextMeta.userName = userRef.current.name;
|
|
1160
1295
|
if (screenRef.current?.name) contextMeta.screenName = screenRef.current.name;
|
|
1161
1296
|
if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
|
|
1162
1297
|
if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
|
|
1163
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);
|
|
1164
1306
|
const report = {
|
|
1165
|
-
id: (0,
|
|
1307
|
+
id: (0, import_core3.newUuid)(),
|
|
1166
1308
|
projectId: config.projectId,
|
|
1167
1309
|
category: data.category,
|
|
1168
1310
|
description: data.description,
|
|
@@ -1183,7 +1325,14 @@ function MushiProvider({ children, ...config }) {
|
|
|
1183
1325
|
})) ?? [],
|
|
1184
1326
|
screenshotDataUrl: data.screenshotDataUrl,
|
|
1185
1327
|
metadata: merged,
|
|
1328
|
+
sessionId: sessionIdRef.current,
|
|
1186
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 } : {},
|
|
1187
1336
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1188
1337
|
};
|
|
1189
1338
|
const client = apiClientRef.current;
|
|
@@ -1241,11 +1390,53 @@ function MushiProvider({ children, ...config }) {
|
|
|
1241
1390
|
provider: traits?.provider
|
|
1242
1391
|
};
|
|
1243
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
|
+
},
|
|
1244
1417
|
setMetadata(key, value) {
|
|
1245
1418
|
metadataRef.current[key] = value;
|
|
1246
1419
|
},
|
|
1247
1420
|
setScreen(screen) {
|
|
1421
|
+
const prev = screenRef.current?.name;
|
|
1248
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);
|
|
1249
1440
|
},
|
|
1250
1441
|
// Reporter API — scoped to the device's persistent reporter token
|
|
1251
1442
|
async listMyReports() {
|