@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/CHANGELOG.md +951 -0
- package/CONTRIBUTING.md +50 -9
- package/README.md +36 -1
- package/dist/index.cjs +221 -22
- package/dist/index.d.cts +40 -4
- package/dist/index.d.ts +40 -4
- package/dist/index.js +218 -17
- package/package.json +8 -7
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
|
]
|
|
@@ -662,7 +697,13 @@ var MushiBottomSheet = ({
|
|
|
662
697
|
var s = import_react_native2.StyleSheet.create({
|
|
663
698
|
flex: { flex: 1 },
|
|
664
699
|
backdrop: {
|
|
665
|
-
|
|
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 ? "
|
|
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:
|
|
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
|
|
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 ??
|
|
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,
|
|
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,
|
|
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,
|
|
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)
|
|
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,
|
|
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() {
|