@mushi-mushi/react-native 0.8.2 → 0.12.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/CONTRIBUTING.md +27 -0
- package/README.md +59 -36
- package/dist/index.cjs +113 -31
- package/dist/index.d.cts +40 -2
- package/dist/index.d.ts +40 -2
- package/dist/index.js +120 -29
- package/package.json +7 -7
package/CONTRIBUTING.md
CHANGED
|
@@ -91,6 +91,33 @@ pnpm changeset
|
|
|
91
91
|
|
|
92
92
|
Select the affected packages, the semver bump type, and write a summary. The changeset file gets committed with your PR.
|
|
93
93
|
|
|
94
|
+
## Release flow
|
|
95
|
+
|
|
96
|
+
Releases are fully automated. Maintainers don't run `npm publish` by hand.
|
|
97
|
+
|
|
98
|
+
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.
|
|
101
|
+
|
|
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**.
|
|
103
|
+
|
|
104
|
+
### Adding a brand-new publishable package
|
|
105
|
+
|
|
106
|
+
Trusted Publisher bindings are configured **per package** on `npmjs.com` and require the package to already exist on the registry. New packages therefore need a one-time bootstrap before OIDC can take over.
|
|
107
|
+
|
|
108
|
+
1. Add the package under `packages/<name>/` with a real `version`, `files`, `publishConfig.access: "public"`, `LICENSE`, and the standard fields enforced by `pnpm check:publish-readiness`.
|
|
109
|
+
2. Build it locally: `pnpm install && pnpm -r build`.
|
|
110
|
+
3. Mint a short-lived granular access token at `https://www.npmjs.com/settings/<your-user>/tokens/granular-access-tokens/new` — **Bypass 2FA: ON**, **Read and write: All packages**, **Expiration: 7 days**.
|
|
111
|
+
4. Bootstrap-publish:
|
|
112
|
+
```bash
|
|
113
|
+
NPM_TOKEN=npm_xxx pnpm bootstrap:new-package
|
|
114
|
+
```
|
|
115
|
+
The script auto-detects which workspace packages are missing on npm and publishes them via `pnpm publish --no-provenance` (so `workspace:^` specifiers get rewritten to real semver in the tarball).
|
|
116
|
+
5. The script prints one URL per freshly-published package. Open each, click **GitHub Actions** under "Trusted Publisher", confirm the auto-filled fields (`<owner>` / `<repo>` / `release.yml`), and tap your security key.
|
|
117
|
+
6. Revoke the bootstrap token at `https://www.npmjs.com/settings/<your-user>/tokens`.
|
|
118
|
+
|
|
119
|
+
From the next changeset bump onward, that package publishes through the normal `release.yml` flow with full OIDC provenance — same as the rest.
|
|
120
|
+
|
|
94
121
|
## Code Style
|
|
95
122
|
|
|
96
123
|
- **TypeScript strict mode** — no `any` unless absolutely necessary
|
package/README.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# @mushi-mushi/react-native
|
|
2
2
|
|
|
3
|
-
React Native SDK for Mushi Mushi —
|
|
3
|
+
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
4
|
|
|
5
|
-
> **One-command setup:** `npx mushi-mushi` auto-detects React Native / Expo and installs this package.
|
|
5
|
+
> **One-command setup:** `npx mushi-mushi` auto-detects React Native / Expo and installs this package with the right env vars.
|
|
6
6
|
>
|
|
7
|
-
> **Other frameworks:** [`@mushi-mushi/react`](https://npmjs.com/package/@mushi-mushi/react) · [`@mushi-mushi/vue`](https://npmjs.com/package/@mushi-mushi/vue) · [`@mushi-mushi/svelte`](https://npmjs.com/package/@mushi-mushi/svelte) · [`@mushi-mushi/angular`](https://npmjs.com/package/@mushi-mushi/angular) · [`@mushi-mushi/capacitor`](https://npmjs.com/package/@mushi-mushi/capacitor)
|
|
7
|
+
> **Other frameworks:** [`@mushi-mushi/react`](https://npmjs.com/package/@mushi-mushi/react) · [`@mushi-mushi/vue`](https://npmjs.com/package/@mushi-mushi/vue) · [`@mushi-mushi/svelte`](https://npmjs.com/package/@mushi-mushi/svelte) · [`@mushi-mushi/angular`](https://npmjs.com/package/@mushi-mushi/angular) · [`@mushi-mushi/capacitor`](https://npmjs.com/package/@mushi-mushi/capacitor) · [`@mushi-mushi/web`](https://npmjs.com/package/@mushi-mushi/web)
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## What this package adds on mobile
|
|
10
|
+
|
|
11
|
+
Every bug your users feel — a button that won't tap, a screen that freezes, a layout that folds on their device — gets captured, classified by AI, and fed into a lesson library that prevents the same class of mistake in the next PR review and the next AI agent run. That's the closed loop the main [`mushi-mushi`](https://www.npmjs.com/package/mushi-mushi) README describes. This package is the mobile entry point.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
10
14
|
|
|
11
15
|
```tsx
|
|
12
16
|
import { MushiProvider } from '@mushi-mushi/react-native'
|
|
@@ -20,74 +24,93 @@ export default function App() {
|
|
|
20
24
|
}
|
|
21
25
|
```
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
That renders a floating 🐛 button and a slide-up bottom-sheet report form. The button stays out of the way until tapped. Report data — screenshot, route, device context, last 5 seconds of console and network — flows to your admin console and is classified within seconds.
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|-------|----------|
|
|
27
|
-
| `'button'` (default) | Shows floating action button |
|
|
28
|
-
| `'shake'` | Installs a shake-to-report listener (no visible UI) |
|
|
29
|
-
| `'both'` | Floating button + shake listener |
|
|
30
|
-
| `'manual'` / `'hidden'` / `'none'` | No auto-UI — call `open()` programmatically |
|
|
31
|
-
| `'attach'` | Hides the default UI; the host renders its own button and calls `attachTo()` |
|
|
29
|
+
## Widget trigger modes
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
it once with `expo install expo-sensors` (or `npm install expo-sensors`); the
|
|
35
|
-
listener is wired lazily so apps that ship `'button'` only never pay the cost.
|
|
31
|
+
Control how users open the report form:
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
| Value | Behavior |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `'button'` (default) | Floating action button — safe on all Hermes builds |
|
|
36
|
+
| `'shake'` | Shake-to-report listener (requires optional `expo-sensors` peer) |
|
|
37
|
+
| `'both'` | Floating button + shake |
|
|
38
|
+
| `'manual'` / `'hidden'` / `'none'` | No auto UI — call `open()` programmatically |
|
|
39
|
+
| `'attach'` | Hides default UI; host renders its own button and calls `attachTo()` |
|
|
40
|
+
|
|
41
|
+
> **Note on shake + Hermes:** `expo-sensors` is loaded lazily via `require()` so apps that ship `trigger: 'button'` never pay the shake-sensor cost. Both triggers are fully Hermes-compatible as of v0.11.0.
|
|
40
42
|
|
|
41
43
|
```tsx
|
|
42
44
|
<MushiProvider
|
|
43
45
|
projectId="proj_xxx"
|
|
44
46
|
apiKey="mushi_xxx"
|
|
45
47
|
config={{
|
|
46
|
-
widget: {
|
|
48
|
+
widget: {
|
|
49
|
+
trigger: 'both',
|
|
50
|
+
buttonPosition: 'bottom-right',
|
|
51
|
+
inset: { bottom: 96, right: 16 }, // clear your tab bar
|
|
52
|
+
},
|
|
47
53
|
}}
|
|
48
54
|
>
|
|
49
55
|
<Navigation />
|
|
50
56
|
</MushiProvider>
|
|
51
57
|
```
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
## Hooks
|
|
54
60
|
|
|
55
61
|
```ts
|
|
56
62
|
import { useMushi, useMushiReport, useMushiWidget } from '@mushi-mushi/react-native'
|
|
57
63
|
|
|
58
64
|
const { submitReport } = useMushiReport()
|
|
59
65
|
const { open, close } = useMushiWidget()
|
|
66
|
+
const mushi = useMushi() // full SDK instance: show(), hide(), setTrigger(), attachTo()
|
|
60
67
|
```
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
## Headless / bring your own button
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { MushiTrigger } from '@mushi-mushi/react-native'
|
|
73
|
+
|
|
74
|
+
<MushiTrigger>
|
|
75
|
+
<Pressable style={styles.btn}>
|
|
76
|
+
<Text>Report a bug</Text>
|
|
77
|
+
</Pressable>
|
|
78
|
+
</MushiTrigger>
|
|
79
|
+
```
|
|
65
80
|
|
|
66
|
-
|
|
81
|
+
`MushiTrigger` wraps any child with `cloneElement` — the child's `onPress` chain is preserved.
|
|
67
82
|
|
|
68
|
-
|
|
83
|
+
## Standalone components
|
|
69
84
|
|
|
70
85
|
```tsx
|
|
71
86
|
import { MushiBottomSheet, MushiFloatingButton } from '@mushi-mushi/react-native'
|
|
72
87
|
```
|
|
73
88
|
|
|
74
|
-
- `MushiBottomSheet` — Conversational
|
|
75
|
-
- `MushiFloatingButton` — Positioned FAB with spring animation. Configurable via `buttonPosition`.
|
|
89
|
+
- `MushiBottomSheet` — Conversational report modal with category → description → submit flow. Dark/light theme, drag-to-dismiss.
|
|
90
|
+
- `MushiFloatingButton` — Positioned FAB with spring animation. Configurable via `buttonPosition` and `inset`.
|
|
76
91
|
|
|
77
|
-
|
|
92
|
+
## What gets captured
|
|
78
93
|
|
|
79
|
-
- Device info (platform, OS, screen dimensions)
|
|
80
|
-
- Console logs (monkey-patched)
|
|
81
|
-
- Network requests (fetch interceptor)
|
|
82
|
-
- Navigation events (React Navigation, optional)
|
|
83
|
-
- Offline queue via
|
|
94
|
+
- Device info (platform, OS version, screen dimensions)
|
|
95
|
+
- Console logs (monkey-patched `console.*`)
|
|
96
|
+
- Network requests (fetch interceptor, headers redacted)
|
|
97
|
+
- Navigation events (React Navigation integration, optional)
|
|
98
|
+
- Offline queue via `@react-native-async-storage/async-storage` — reports survive force-close
|
|
84
99
|
|
|
85
|
-
## Peer
|
|
100
|
+
## Peer dependencies
|
|
86
101
|
|
|
87
102
|
- `react` >= 18
|
|
88
103
|
- `react-native` >= 0.72
|
|
89
|
-
- `@react-navigation/native` >= 6 (optional)
|
|
90
|
-
- `@react-native-async-storage/async-storage` >= 1.19 (optional)
|
|
104
|
+
- `@react-navigation/native` >= 6 (optional — navigation capture)
|
|
105
|
+
- `@react-native-async-storage/async-storage` >= 1.19 (optional — offline queue)
|
|
106
|
+
- `expo-sensors` (optional — shake trigger only)
|
|
107
|
+
|
|
108
|
+
## Changelog highlights
|
|
109
|
+
|
|
110
|
+
- **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
|
+
- **v0.8.x** — Shake-to-report listener, offline queue, navigation capture.
|
|
112
|
+
|
|
113
|
+
See the full [CHANGELOG](https://github.com/kensaurus/mushi-mushi/blob/master/packages/react-native/CHANGELOG.md).
|
|
91
114
|
|
|
92
115
|
## License
|
|
93
116
|
|
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
MushiBottomSheet: () => MushiBottomSheet,
|
|
35
35
|
MushiFloatingButton: () => MushiFloatingButton,
|
|
36
36
|
MushiProvider: () => MushiProvider,
|
|
37
|
+
MushiTrigger: () => MushiTrigger,
|
|
37
38
|
getDeviceInfo: () => getDeviceInfo,
|
|
38
39
|
setupConsoleCapture: () => setupConsoleCapture,
|
|
39
40
|
setupNetworkCapture: () => setupNetworkCapture,
|
|
@@ -714,18 +715,16 @@ function MushiProvider({ children, ...config }) {
|
|
|
714
715
|
}, []);
|
|
715
716
|
(0, import_react3.useEffect)(() => {
|
|
716
717
|
let unsubscribe;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
})();
|
|
718
|
+
try {
|
|
719
|
+
const NetInfo = require("@react-native-community/netinfo").default;
|
|
720
|
+
unsubscribe = NetInfo.addEventListener((state) => {
|
|
721
|
+
if (state.isConnected && state.isInternetReachable) {
|
|
722
|
+
queueRef.current?.flush().catch(() => {
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
729
728
|
return () => unsubscribe?.();
|
|
730
729
|
}, []);
|
|
731
730
|
const open = (0, import_react3.useCallback)(() => setSheetVisible(true), []);
|
|
@@ -738,20 +737,22 @@ function MushiProvider({ children, ...config }) {
|
|
|
738
737
|
let cleanup;
|
|
739
738
|
let lastShake = 0;
|
|
740
739
|
const threshold = config.widget?.shakeThreshold ?? 2.7;
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
740
|
+
try {
|
|
741
|
+
const mod = require("expo-sensors");
|
|
742
|
+
if (!disposed) {
|
|
743
|
+
mod.Accelerometer.setUpdateInterval(120);
|
|
744
|
+
const sub = mod.Accelerometer.addListener((evt) => {
|
|
745
|
+
const g = Math.sqrt(evt.x * evt.x + evt.y * evt.y + evt.z * evt.z);
|
|
746
|
+
if (g < threshold) return;
|
|
747
|
+
const now = Date.now();
|
|
748
|
+
if (now - lastShake < 1e3) return;
|
|
749
|
+
lastShake = now;
|
|
750
|
+
open();
|
|
751
|
+
});
|
|
752
|
+
cleanup = () => sub.remove();
|
|
753
|
+
}
|
|
754
|
+
} catch {
|
|
755
|
+
}
|
|
755
756
|
return () => {
|
|
756
757
|
disposed = true;
|
|
757
758
|
cleanup?.();
|
|
@@ -792,6 +793,31 @@ function MushiProvider({ children, ...config }) {
|
|
|
792
793
|
},
|
|
793
794
|
[config.projectId, config.apiKey, apiEndpoint]
|
|
794
795
|
);
|
|
796
|
+
const userRef = (0, import_react3.useRef)(null);
|
|
797
|
+
const metadataRef = (0, import_react3.useRef)({});
|
|
798
|
+
const screenRef = (0, import_react3.useRef)(null);
|
|
799
|
+
const activityQueueRef = (0, import_react3.useRef)([]);
|
|
800
|
+
const rewardsFlushRef = (0, import_react3.useRef)(null);
|
|
801
|
+
(0, import_react3.useEffect)(() => {
|
|
802
|
+
const rewards = config.rewards;
|
|
803
|
+
if (!rewards?.enabled || !rewards.trackActivity) return;
|
|
804
|
+
const flushMs = Math.max(3e4, rewards.flushIntervalMs ?? 3e5);
|
|
805
|
+
rewardsFlushRef.current = setInterval(async () => {
|
|
806
|
+
const userId = userRef.current?.id;
|
|
807
|
+
const client = apiClientRef.current;
|
|
808
|
+
if (!userId || !client || activityQueueRef.current.length === 0) return;
|
|
809
|
+
const batch = activityQueueRef.current.splice(0, 100);
|
|
810
|
+
await client.submitActivity(userId, batch, {
|
|
811
|
+
userTraits: userRef.current ?? void 0,
|
|
812
|
+
optedIn: true
|
|
813
|
+
}).catch(() => {
|
|
814
|
+
activityQueueRef.current.unshift(...batch.slice(0, 50));
|
|
815
|
+
});
|
|
816
|
+
}, flushMs);
|
|
817
|
+
return () => {
|
|
818
|
+
if (rewardsFlushRef.current) clearInterval(rewardsFlushRef.current);
|
|
819
|
+
};
|
|
820
|
+
}, [config.rewards?.enabled, config.rewards?.trackActivity, config.rewards?.flushIntervalMs]);
|
|
795
821
|
const instance = (0, import_react3.useMemo)(
|
|
796
822
|
() => ({
|
|
797
823
|
open,
|
|
@@ -800,9 +826,50 @@ function MushiProvider({ children, ...config }) {
|
|
|
800
826
|
submitReport,
|
|
801
827
|
getDeviceInfo,
|
|
802
828
|
getConsoleEntries: () => consoleRef.current?.getEntries() ?? [],
|
|
803
|
-
getNetworkEntries: () => networkRef.current?.getEntries() ?? []
|
|
829
|
+
getNetworkEntries: () => networkRef.current?.getEntries() ?? [],
|
|
830
|
+
// v0.10.0: new identity methods (these were causing workarounds in apps/mobile/src/native/mushi.ts)
|
|
831
|
+
identify(userId, traits) {
|
|
832
|
+
userRef.current = {
|
|
833
|
+
id: userId,
|
|
834
|
+
email: traits?.email,
|
|
835
|
+
name: traits?.name,
|
|
836
|
+
provider: traits?.provider
|
|
837
|
+
};
|
|
838
|
+
},
|
|
839
|
+
setMetadata(key, value) {
|
|
840
|
+
metadataRef.current[key] = value;
|
|
841
|
+
},
|
|
842
|
+
setScreen(screen) {
|
|
843
|
+
screenRef.current = screen;
|
|
844
|
+
},
|
|
845
|
+
// Rewards (P1)
|
|
846
|
+
recordActivity(action, metadata) {
|
|
847
|
+
if (!config.rewards?.enabled) return;
|
|
848
|
+
activityQueueRef.current.push({ action, metadata });
|
|
849
|
+
},
|
|
850
|
+
async getTier() {
|
|
851
|
+
const userId = userRef.current?.id;
|
|
852
|
+
const client = apiClientRef.current;
|
|
853
|
+
if (!userId || !client) return null;
|
|
854
|
+
const res = await client.getMyTier(userId);
|
|
855
|
+
return res.ok ? res.data : null;
|
|
856
|
+
},
|
|
857
|
+
async getReputation() {
|
|
858
|
+
const userId = userRef.current?.id;
|
|
859
|
+
const client = apiClientRef.current;
|
|
860
|
+
if (!userId || !client) return null;
|
|
861
|
+
const res = await client.getMyPoints(userId);
|
|
862
|
+
if (!res.ok) return null;
|
|
863
|
+
return {
|
|
864
|
+
totalPoints: res.data.total_points ?? 0,
|
|
865
|
+
points30d: res.data.points_30d ?? 0,
|
|
866
|
+
reputation: 1,
|
|
867
|
+
confirmedBugs: 0,
|
|
868
|
+
totalReports: 0
|
|
869
|
+
};
|
|
870
|
+
}
|
|
804
871
|
}),
|
|
805
|
-
[open, close, attachTo, submitReport]
|
|
872
|
+
[open, close, attachTo, submitReport, config.rewards?.enabled]
|
|
806
873
|
);
|
|
807
874
|
const trigger = config.widget?.trigger ?? "button";
|
|
808
875
|
const showFab = trigger === "button" || trigger === "both" || trigger === "auto" || trigger === "edge-tab";
|
|
@@ -852,11 +919,25 @@ function useMushiWidget() {
|
|
|
852
919
|
};
|
|
853
920
|
}
|
|
854
921
|
|
|
855
|
-
// src/
|
|
922
|
+
// src/components/MushiTrigger.tsx
|
|
856
923
|
var import_react6 = require("react");
|
|
924
|
+
function MushiTrigger({ children }) {
|
|
925
|
+
const mushi = useMushiContext();
|
|
926
|
+
if (!(0, import_react6.isValidElement)(children)) return children;
|
|
927
|
+
const originalOnPress = children.props.onPress;
|
|
928
|
+
return (0, import_react6.cloneElement)(children, {
|
|
929
|
+
onPress: () => {
|
|
930
|
+
originalOnPress?.();
|
|
931
|
+
mushi?.open();
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// src/capture/navigation-capture.ts
|
|
937
|
+
var import_react7 = require("react");
|
|
857
938
|
function useNavigationCapture(navigationRef) {
|
|
858
|
-
const historyRef = (0,
|
|
859
|
-
(0,
|
|
939
|
+
const historyRef = (0, import_react7.useRef)([]);
|
|
940
|
+
(0, import_react7.useEffect)(() => {
|
|
860
941
|
if (!navigationRef?.addListener || !navigationRef?.getCurrentRoute) return;
|
|
861
942
|
const unsubscribe = navigationRef.addListener("state", () => {
|
|
862
943
|
const route = navigationRef.getCurrentRoute?.();
|
|
@@ -875,6 +956,7 @@ function useNavigationCapture(navigationRef) {
|
|
|
875
956
|
MushiBottomSheet,
|
|
876
957
|
MushiFloatingButton,
|
|
877
958
|
MushiProvider,
|
|
959
|
+
MushiTrigger,
|
|
878
960
|
getDeviceInfo,
|
|
879
961
|
setupConsoleCapture,
|
|
880
962
|
setupNetworkCapture,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { ReactNode, FC, ReactElement } from 'react';
|
|
4
|
+
import { MushiRewardsConfig, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
|
|
3
5
|
|
|
4
6
|
interface ConsoleEntry {
|
|
5
7
|
level: 'log' | 'warn' | 'error';
|
|
@@ -65,6 +67,7 @@ interface MushiRNConfig {
|
|
|
65
67
|
maxQueueSize?: number;
|
|
66
68
|
retryIntervalMs?: number;
|
|
67
69
|
};
|
|
70
|
+
rewards?: MushiRewardsConfig;
|
|
68
71
|
}
|
|
69
72
|
interface MushiRNInstance {
|
|
70
73
|
open(): void;
|
|
@@ -79,6 +82,27 @@ interface MushiRNInstance {
|
|
|
79
82
|
getDeviceInfo(): ReturnType<typeof getDeviceInfo>;
|
|
80
83
|
getConsoleEntries(): ReturnType<ReturnType<typeof setupConsoleCapture>['getEntries']>;
|
|
81
84
|
getNetworkEntries(): ReturnType<ReturnType<typeof setupNetworkCapture>['getEntries']>;
|
|
85
|
+
/** Set the current authenticated user. Equivalent to Mushi.identify() on web. */
|
|
86
|
+
identify(userId: string, traits?: {
|
|
87
|
+
email?: string;
|
|
88
|
+
name?: string;
|
|
89
|
+
provider?: string;
|
|
90
|
+
[k: string]: unknown;
|
|
91
|
+
}): void;
|
|
92
|
+
/** Attach arbitrary key/value metadata to subsequent reports. */
|
|
93
|
+
setMetadata(key: string, value: unknown): void;
|
|
94
|
+
/** Set the current screen context attached to subsequent reports. */
|
|
95
|
+
setScreen(screen: {
|
|
96
|
+
name: string;
|
|
97
|
+
route?: string;
|
|
98
|
+
feature?: string;
|
|
99
|
+
}): void;
|
|
100
|
+
/** Manually record a host-defined activity event. */
|
|
101
|
+
recordActivity(action: string, metadata?: Record<string, unknown>): void;
|
|
102
|
+
/** Returns the current user's tier. */
|
|
103
|
+
getTier(): Promise<MushiTierResult | null>;
|
|
104
|
+
/** Returns the current user's reputation + point totals. */
|
|
105
|
+
getReputation(): Promise<MushiReputationResult | null>;
|
|
82
106
|
}
|
|
83
107
|
declare function MushiProvider({ children, ...config }: MushiRNConfig & {
|
|
84
108
|
children: ReactNode;
|
|
@@ -167,6 +191,20 @@ interface MushiFloatingButtonProps {
|
|
|
167
191
|
}
|
|
168
192
|
declare const MushiFloatingButton: FC<MushiFloatingButtonProps>;
|
|
169
193
|
|
|
194
|
+
interface MushiTriggerProps {
|
|
195
|
+
/** A single React Native element that accepts `onPress`. */
|
|
196
|
+
children: ReactElement<{
|
|
197
|
+
onPress?: () => void;
|
|
198
|
+
}>;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Clones `children` and injects `onPress` → `mushi.open()`.
|
|
202
|
+
* The child is responsible for its own visual styling.
|
|
203
|
+
*/
|
|
204
|
+
declare function MushiTrigger({ children }: MushiTriggerProps): ReactElement<{
|
|
205
|
+
onPress?: () => void;
|
|
206
|
+
}, string | react.JSXElementConstructor<any>>;
|
|
207
|
+
|
|
170
208
|
interface NavigationEntry {
|
|
171
209
|
screen: string;
|
|
172
210
|
timestamp: number;
|
|
@@ -195,4 +233,4 @@ declare class AsyncStorageQueue {
|
|
|
195
233
|
private getAsyncStorage;
|
|
196
234
|
}
|
|
197
235
|
|
|
198
|
-
export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MushiBottomSheet, type MushiBottomSheetProps, MushiFloatingButton, type MushiFloatingButtonProps, MushiProvider, type MushiRNConfig, type MushiRNInstance, type NavigationEntry, type NetworkEntry, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
|
|
236
|
+
export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MushiBottomSheet, type MushiBottomSheetProps, MushiFloatingButton, type MushiFloatingButtonProps, MushiProvider, type MushiRNConfig, type MushiRNInstance, MushiTrigger, type MushiTriggerProps, type NavigationEntry, type NetworkEntry, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { ReactNode, FC, ReactElement } from 'react';
|
|
4
|
+
import { MushiRewardsConfig, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
|
|
3
5
|
|
|
4
6
|
interface ConsoleEntry {
|
|
5
7
|
level: 'log' | 'warn' | 'error';
|
|
@@ -65,6 +67,7 @@ interface MushiRNConfig {
|
|
|
65
67
|
maxQueueSize?: number;
|
|
66
68
|
retryIntervalMs?: number;
|
|
67
69
|
};
|
|
70
|
+
rewards?: MushiRewardsConfig;
|
|
68
71
|
}
|
|
69
72
|
interface MushiRNInstance {
|
|
70
73
|
open(): void;
|
|
@@ -79,6 +82,27 @@ interface MushiRNInstance {
|
|
|
79
82
|
getDeviceInfo(): ReturnType<typeof getDeviceInfo>;
|
|
80
83
|
getConsoleEntries(): ReturnType<ReturnType<typeof setupConsoleCapture>['getEntries']>;
|
|
81
84
|
getNetworkEntries(): ReturnType<ReturnType<typeof setupNetworkCapture>['getEntries']>;
|
|
85
|
+
/** Set the current authenticated user. Equivalent to Mushi.identify() on web. */
|
|
86
|
+
identify(userId: string, traits?: {
|
|
87
|
+
email?: string;
|
|
88
|
+
name?: string;
|
|
89
|
+
provider?: string;
|
|
90
|
+
[k: string]: unknown;
|
|
91
|
+
}): void;
|
|
92
|
+
/** Attach arbitrary key/value metadata to subsequent reports. */
|
|
93
|
+
setMetadata(key: string, value: unknown): void;
|
|
94
|
+
/** Set the current screen context attached to subsequent reports. */
|
|
95
|
+
setScreen(screen: {
|
|
96
|
+
name: string;
|
|
97
|
+
route?: string;
|
|
98
|
+
feature?: string;
|
|
99
|
+
}): void;
|
|
100
|
+
/** Manually record a host-defined activity event. */
|
|
101
|
+
recordActivity(action: string, metadata?: Record<string, unknown>): void;
|
|
102
|
+
/** Returns the current user's tier. */
|
|
103
|
+
getTier(): Promise<MushiTierResult | null>;
|
|
104
|
+
/** Returns the current user's reputation + point totals. */
|
|
105
|
+
getReputation(): Promise<MushiReputationResult | null>;
|
|
82
106
|
}
|
|
83
107
|
declare function MushiProvider({ children, ...config }: MushiRNConfig & {
|
|
84
108
|
children: ReactNode;
|
|
@@ -167,6 +191,20 @@ interface MushiFloatingButtonProps {
|
|
|
167
191
|
}
|
|
168
192
|
declare const MushiFloatingButton: FC<MushiFloatingButtonProps>;
|
|
169
193
|
|
|
194
|
+
interface MushiTriggerProps {
|
|
195
|
+
/** A single React Native element that accepts `onPress`. */
|
|
196
|
+
children: ReactElement<{
|
|
197
|
+
onPress?: () => void;
|
|
198
|
+
}>;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Clones `children` and injects `onPress` → `mushi.open()`.
|
|
202
|
+
* The child is responsible for its own visual styling.
|
|
203
|
+
*/
|
|
204
|
+
declare function MushiTrigger({ children }: MushiTriggerProps): ReactElement<{
|
|
205
|
+
onPress?: () => void;
|
|
206
|
+
}, string | react.JSXElementConstructor<any>>;
|
|
207
|
+
|
|
170
208
|
interface NavigationEntry {
|
|
171
209
|
screen: string;
|
|
172
210
|
timestamp: number;
|
|
@@ -195,4 +233,4 @@ declare class AsyncStorageQueue {
|
|
|
195
233
|
private getAsyncStorage;
|
|
196
234
|
}
|
|
197
235
|
|
|
198
|
-
export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MushiBottomSheet, type MushiBottomSheetProps, MushiFloatingButton, type MushiFloatingButtonProps, MushiProvider, type MushiRNConfig, type MushiRNInstance, type NavigationEntry, type NetworkEntry, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
|
|
236
|
+
export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MushiBottomSheet, type MushiBottomSheetProps, MushiFloatingButton, type MushiFloatingButtonProps, MushiProvider, type MushiRNConfig, type MushiRNInstance, MushiTrigger, type MushiTriggerProps, type NavigationEntry, type NetworkEntry, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/provider.tsx
|
|
2
9
|
import {
|
|
3
10
|
createContext,
|
|
@@ -8,7 +15,10 @@ import {
|
|
|
8
15
|
useCallback as useCallback3,
|
|
9
16
|
useMemo
|
|
10
17
|
} from "react";
|
|
11
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
createApiClient,
|
|
20
|
+
DEFAULT_API_ENDPOINT
|
|
21
|
+
} from "@mushi-mushi/core";
|
|
12
22
|
|
|
13
23
|
// src/capture/console-capture.ts
|
|
14
24
|
function setupConsoleCapture(maxEntries = 100) {
|
|
@@ -700,18 +710,16 @@ function MushiProvider({ children, ...config }) {
|
|
|
700
710
|
}, []);
|
|
701
711
|
useEffect2(() => {
|
|
702
712
|
let unsubscribe;
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
})();
|
|
713
|
+
try {
|
|
714
|
+
const NetInfo = __require("@react-native-community/netinfo").default;
|
|
715
|
+
unsubscribe = NetInfo.addEventListener((state) => {
|
|
716
|
+
if (state.isConnected && state.isInternetReachable) {
|
|
717
|
+
queueRef.current?.flush().catch(() => {
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
715
723
|
return () => unsubscribe?.();
|
|
716
724
|
}, []);
|
|
717
725
|
const open = useCallback3(() => setSheetVisible(true), []);
|
|
@@ -724,20 +732,22 @@ function MushiProvider({ children, ...config }) {
|
|
|
724
732
|
let cleanup;
|
|
725
733
|
let lastShake = 0;
|
|
726
734
|
const threshold = config.widget?.shakeThreshold ?? 2.7;
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
735
|
+
try {
|
|
736
|
+
const mod = __require("expo-sensors");
|
|
737
|
+
if (!disposed) {
|
|
738
|
+
mod.Accelerometer.setUpdateInterval(120);
|
|
739
|
+
const sub = mod.Accelerometer.addListener((evt) => {
|
|
740
|
+
const g = Math.sqrt(evt.x * evt.x + evt.y * evt.y + evt.z * evt.z);
|
|
741
|
+
if (g < threshold) return;
|
|
742
|
+
const now = Date.now();
|
|
743
|
+
if (now - lastShake < 1e3) return;
|
|
744
|
+
lastShake = now;
|
|
745
|
+
open();
|
|
746
|
+
});
|
|
747
|
+
cleanup = () => sub.remove();
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
741
751
|
return () => {
|
|
742
752
|
disposed = true;
|
|
743
753
|
cleanup?.();
|
|
@@ -778,6 +788,31 @@ function MushiProvider({ children, ...config }) {
|
|
|
778
788
|
},
|
|
779
789
|
[config.projectId, config.apiKey, apiEndpoint]
|
|
780
790
|
);
|
|
791
|
+
const userRef = useRef3(null);
|
|
792
|
+
const metadataRef = useRef3({});
|
|
793
|
+
const screenRef = useRef3(null);
|
|
794
|
+
const activityQueueRef = useRef3([]);
|
|
795
|
+
const rewardsFlushRef = useRef3(null);
|
|
796
|
+
useEffect2(() => {
|
|
797
|
+
const rewards = config.rewards;
|
|
798
|
+
if (!rewards?.enabled || !rewards.trackActivity) return;
|
|
799
|
+
const flushMs = Math.max(3e4, rewards.flushIntervalMs ?? 3e5);
|
|
800
|
+
rewardsFlushRef.current = setInterval(async () => {
|
|
801
|
+
const userId = userRef.current?.id;
|
|
802
|
+
const client = apiClientRef.current;
|
|
803
|
+
if (!userId || !client || activityQueueRef.current.length === 0) return;
|
|
804
|
+
const batch = activityQueueRef.current.splice(0, 100);
|
|
805
|
+
await client.submitActivity(userId, batch, {
|
|
806
|
+
userTraits: userRef.current ?? void 0,
|
|
807
|
+
optedIn: true
|
|
808
|
+
}).catch(() => {
|
|
809
|
+
activityQueueRef.current.unshift(...batch.slice(0, 50));
|
|
810
|
+
});
|
|
811
|
+
}, flushMs);
|
|
812
|
+
return () => {
|
|
813
|
+
if (rewardsFlushRef.current) clearInterval(rewardsFlushRef.current);
|
|
814
|
+
};
|
|
815
|
+
}, [config.rewards?.enabled, config.rewards?.trackActivity, config.rewards?.flushIntervalMs]);
|
|
781
816
|
const instance = useMemo(
|
|
782
817
|
() => ({
|
|
783
818
|
open,
|
|
@@ -786,9 +821,50 @@ function MushiProvider({ children, ...config }) {
|
|
|
786
821
|
submitReport,
|
|
787
822
|
getDeviceInfo,
|
|
788
823
|
getConsoleEntries: () => consoleRef.current?.getEntries() ?? [],
|
|
789
|
-
getNetworkEntries: () => networkRef.current?.getEntries() ?? []
|
|
824
|
+
getNetworkEntries: () => networkRef.current?.getEntries() ?? [],
|
|
825
|
+
// v0.10.0: new identity methods (these were causing workarounds in apps/mobile/src/native/mushi.ts)
|
|
826
|
+
identify(userId, traits) {
|
|
827
|
+
userRef.current = {
|
|
828
|
+
id: userId,
|
|
829
|
+
email: traits?.email,
|
|
830
|
+
name: traits?.name,
|
|
831
|
+
provider: traits?.provider
|
|
832
|
+
};
|
|
833
|
+
},
|
|
834
|
+
setMetadata(key, value) {
|
|
835
|
+
metadataRef.current[key] = value;
|
|
836
|
+
},
|
|
837
|
+
setScreen(screen) {
|
|
838
|
+
screenRef.current = screen;
|
|
839
|
+
},
|
|
840
|
+
// Rewards (P1)
|
|
841
|
+
recordActivity(action, metadata) {
|
|
842
|
+
if (!config.rewards?.enabled) return;
|
|
843
|
+
activityQueueRef.current.push({ action, metadata });
|
|
844
|
+
},
|
|
845
|
+
async getTier() {
|
|
846
|
+
const userId = userRef.current?.id;
|
|
847
|
+
const client = apiClientRef.current;
|
|
848
|
+
if (!userId || !client) return null;
|
|
849
|
+
const res = await client.getMyTier(userId);
|
|
850
|
+
return res.ok ? res.data : null;
|
|
851
|
+
},
|
|
852
|
+
async getReputation() {
|
|
853
|
+
const userId = userRef.current?.id;
|
|
854
|
+
const client = apiClientRef.current;
|
|
855
|
+
if (!userId || !client) return null;
|
|
856
|
+
const res = await client.getMyPoints(userId);
|
|
857
|
+
if (!res.ok) return null;
|
|
858
|
+
return {
|
|
859
|
+
totalPoints: res.data.total_points ?? 0,
|
|
860
|
+
points30d: res.data.points_30d ?? 0,
|
|
861
|
+
reputation: 1,
|
|
862
|
+
confirmedBugs: 0,
|
|
863
|
+
totalReports: 0
|
|
864
|
+
};
|
|
865
|
+
}
|
|
790
866
|
}),
|
|
791
|
-
[open, close, attachTo, submitReport]
|
|
867
|
+
[open, close, attachTo, submitReport, config.rewards?.enabled]
|
|
792
868
|
);
|
|
793
869
|
const trigger = config.widget?.trigger ?? "button";
|
|
794
870
|
const showFab = trigger === "button" || trigger === "both" || trigger === "auto" || trigger === "edge-tab";
|
|
@@ -838,6 +914,20 @@ function useMushiWidget() {
|
|
|
838
914
|
};
|
|
839
915
|
}
|
|
840
916
|
|
|
917
|
+
// src/components/MushiTrigger.tsx
|
|
918
|
+
import { cloneElement, isValidElement } from "react";
|
|
919
|
+
function MushiTrigger({ children }) {
|
|
920
|
+
const mushi = useMushiContext();
|
|
921
|
+
if (!isValidElement(children)) return children;
|
|
922
|
+
const originalOnPress = children.props.onPress;
|
|
923
|
+
return cloneElement(children, {
|
|
924
|
+
onPress: () => {
|
|
925
|
+
originalOnPress?.();
|
|
926
|
+
mushi?.open();
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
841
931
|
// src/capture/navigation-capture.ts
|
|
842
932
|
import { useEffect as useEffect3, useRef as useRef4 } from "react";
|
|
843
933
|
function useNavigationCapture(navigationRef) {
|
|
@@ -860,6 +950,7 @@ export {
|
|
|
860
950
|
MushiBottomSheet,
|
|
861
951
|
MushiFloatingButton,
|
|
862
952
|
MushiProvider,
|
|
953
|
+
MushiTrigger,
|
|
863
954
|
getDeviceInfo,
|
|
864
955
|
setupConsoleCapture,
|
|
865
956
|
setupNetworkCapture,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushi-mushi/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native SDK for Mushi Mushi — shake-to-report, bottom sheet widget, full context capture",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,15 +29,15 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@mushi-mushi/core": "^1.
|
|
32
|
+
"@mushi-mushi/core": "^1.2.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@types/react": "^19.
|
|
36
|
-
"react": "^19.
|
|
37
|
-
"react-native": "^0.79.
|
|
35
|
+
"@types/react": "^19.2.14",
|
|
36
|
+
"react": "^19.2.5",
|
|
37
|
+
"react-native": "^0.79.7",
|
|
38
38
|
"tsup": "^8.5.1",
|
|
39
|
-
"typescript": "^6.0.
|
|
40
|
-
"vitest": "^4.1.
|
|
39
|
+
"typescript": "^6.0.3",
|
|
40
|
+
"vitest": "^4.1.5"
|
|
41
41
|
},
|
|
42
42
|
"author": "Kenji Sakuramoto",
|
|
43
43
|
"repository": {
|