@mushi-mushi/react-native 0.8.2 → 0.11.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/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.11.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.1.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": {