@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 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 — built-in bottom-sheet widget, floating bug button, offline queue.
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) (Ionic) · [`@mushi-mushi/web`](https://npmjs.com/package/@mushi-mushi/web) (vanilla JS)
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
- ## Usage
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
- The provider renders a floating 🐛 button and a slide-up bottom sheet by default. Control behavior via `widget.trigger`:
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
- | Value | Behavior |
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
- Shake detection requires the optional `expo-sensors` peer dependency. Install
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
- `widget.inset` (and the `MushiFloatingButton` `inset` prop) accepts
38
- `{ top?, right?, bottom?, left? }` in points so the trigger can clear tab
39
- bars and host CTAs.
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: { trigger: 'both', inset: { bottom: 96, right: 16 } },
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
- ### Hooks
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
- The provider's `useMushi()` instance also exposes the cross-platform trigger
63
- controls — `show()`, `hide()`, `attachTo(elementRefOrId)`, and
64
- `setTrigger(mode)` — so a host app can toggle the launcher per screen.
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
- ### Standalone Components
81
+ `MushiTrigger` wraps any child with `cloneElement` — the child's `onPress` chain is preserved.
67
82
 
68
- Use the widget components directly if you need custom integration:
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 bug report modal (category → description → submit). Supports dark/light theme, drag-to-dismiss.
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
- ### Captures
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 AsyncStorage
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 Dependencies
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
- (async () => {
718
- try {
719
- const NetInfo = await new Function("s", "return import(s)")("@react-native-community/netinfo");
720
- unsubscribe = NetInfo.default.addEventListener((state) => {
721
- if (state.isConnected && state.isInternetReachable) {
722
- queueRef.current?.flush().catch(() => {
723
- });
724
- }
725
- });
726
- } catch {
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
- new Function("specifier", "return import(specifier)")("expo-sensors").then((mod) => {
742
- if (disposed) return;
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
- }).catch(() => {
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/capture/navigation-capture.ts
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, import_react6.useRef)([]);
859
- (0, import_react6.useEffect)(() => {
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 { ReactNode, FC } from 'react';
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 { ReactNode, FC } from 'react';
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 { createApiClient, DEFAULT_API_ENDPOINT } from "@mushi-mushi/core";
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
- (async () => {
704
- try {
705
- const NetInfo = await new Function("s", "return import(s)")("@react-native-community/netinfo");
706
- unsubscribe = NetInfo.default.addEventListener((state) => {
707
- if (state.isConnected && state.isInternetReachable) {
708
- queueRef.current?.flush().catch(() => {
709
- });
710
- }
711
- });
712
- } catch {
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
- new Function("specifier", "return import(specifier)")("expo-sensors").then((mod) => {
728
- if (disposed) return;
729
- mod.Accelerometer.setUpdateInterval(120);
730
- const sub = mod.Accelerometer.addListener((evt) => {
731
- const g = Math.sqrt(evt.x * evt.x + evt.y * evt.y + evt.z * evt.z);
732
- if (g < threshold) return;
733
- const now = Date.now();
734
- if (now - lastShake < 1e3) return;
735
- lastShake = now;
736
- open();
737
- });
738
- cleanup = () => sub.remove();
739
- }).catch(() => {
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.8.2",
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.0.0"
32
+ "@mushi-mushi/core": "^1.2.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@types/react": "^19.1.4",
36
- "react": "^19.1.0",
37
- "react-native": "^0.79.0",
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.2",
40
- "vitest": "^4.1.4"
39
+ "typescript": "^6.0.3",
40
+ "vitest": "^4.1.5"
41
41
  },
42
42
  "author": "Kenji Sakuramoto",
43
43
  "repository": {