@mushi-mushi/react-native 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @mushi-mushi/react-native
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 26e038f: SDK polish from the UI/UX audit pass:
8
+
9
+ - **web**: the widget now live-restyles when the OS `prefers-color-scheme`
10
+ changes under `theme: 'auto' | 'inherit'`, and mount is hardened so a host
11
+ page whose `matchMedia` lacks the modern `addEventListener` can never throw.
12
+ Richer banner layout (pill + message + flat actions), `hideOnSelector`
13
+ suppression, submit failure-kind surfacing, and expanded i18n copy
14
+ (en/es/ja/th).
15
+ - **react-native**: banner, bottom-sheet, and floating-button polish;
16
+ `submitReport()` now resolves to a typed failure result instead of
17
+ `undefined` when called before the provider finishes initializing.
18
+ - **core**: additional shared design tokens consumed by the surfaces above.
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [26e038f]
23
+ - @mushi-mushi/web@1.26.0
24
+ - @mushi-mushi/core@1.26.0
25
+
26
+ ## 0.20.1
27
+
28
+ ### Patch Changes
29
+
30
+ - 5feac27: # v0.20.1 — Post-release SDK reliability fixes
31
+
32
+ - **React Native types resolve after publish**: removed the `web-i18n.d.ts` ambient shim that re-exported `MushiLocale` from a monorepo-relative source path (`../../web/src/i18n/types`) that does not exist in a published install. `@mushi-mushi/web` already ships proper `./i18n` types via its `exports` map, which `moduleResolution: "bundler"`/`nodenext` resolves directly.
33
+
34
+ - Updated dependencies [5feac27]
35
+ - @mushi-mushi/web@1.21.1
36
+
3
37
  ## 0.20.0
4
38
 
5
39
  ### Minor Changes
package/CONTRIBUTING.md CHANGED
@@ -76,6 +76,7 @@ packages/
76
76
  apps/
77
77
  admin/ Admin dashboard (React + Tailwind)
78
78
  docs/ Documentation site (Nextra — apps/docs)
79
+ testers/ Tester marketplace portal (Next.js)
79
80
  tooling/
80
81
  eslint-config/ Shared ESLint flat config
81
82
  tsconfig/ Shared TypeScript configs
@@ -118,15 +119,22 @@ If your PR changes **user-visible SDK or CLI behavior**, update docs in the **sa
118
119
  Guardrails (run locally before pushing):
119
120
 
120
121
  ```bash
121
- pnpm check:docs-stats # root README stat badges
122
+ pnpm check:docs-stats # root README stat badges + glance <sub> line
122
123
  pnpm check:sdk-version-matrix # apps/docs/content/sdks/index.mdx versions
123
124
  pnpm check:internal-doc-links # dead /foo links in MDX
124
- pnpm check:onboarding-drift # phantom MCP env vars / stale tool names
125
+ pnpm check:onboarding-drift # phantom MCP env vars / stale tool names / native install coords
126
+ pnpm check:catalog-sync # MCP catalog ↔ admin counts ↔ prose "N tools"
127
+ pnpm check:mcp-tools-doc # mcp-tools.generated.mdx stale check
125
128
  pnpm check:bundle-docs # @mushi-mushi/web size-limit vs README
129
+ pnpm check:docs-drift # versions + public docs links
126
130
  pnpm gen:llms-txt # regenerate apps/docs/public/llms.txt after nav changes
127
131
  pnpm gen:mcp-tools-doc # regenerate sdks/mcp-tools.generated.mdx from catalog.ts
128
132
  ```
129
133
 
134
+ **Same-PR rule:** if behavior, env vars, CLI commands, MCP tools, or install
135
+ coordinates change, update the matching docs in this PR and run
136
+ `pnpm check:docs-drift && pnpm check:onboarding-drift && pnpm check:catalog-sync`.
137
+
130
138
  README changes on npm packages only appear on [npmjs.com](https://www.npmjs.com) after the next **Changesets publish**.
131
139
 
132
140
  ## Changesets
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Your AI wrote it. Mushi tells you why it broke.**
4
4
 
5
- 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.
5
+ React Native SDK for [Mushi Mushi](https://www.npmjs.com/package/mushi-mushi) — shake-to-report widget, offline queue, and a plain-English diagnosis in your editor, for iOS + Android.
6
6
 
7
7
  > **One-command setup:** `npx mushi-mushi` auto-detects React Native / Expo and installs this package with the right env vars.
8
8
  >
@@ -12,6 +12,20 @@ React Native SDK for [Mushi Mushi](https://www.npmjs.com/package/mushi-mushi)
12
12
 
13
13
  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.
14
14
 
15
+ ## Error monitoring (optional)
16
+
17
+ Mushi captures user-reported bugs through its own ingest pipeline. If you also run **Sentry** in your React Native app, wire both for full coverage:
18
+
19
+ ```tsx
20
+ import * as Sentry from '@sentry/react-native'
21
+
22
+ Sentry.init({ dsn: process.env.EXPO_PUBLIC_SENTRY_DSN })
23
+
24
+ // Mushi handles shake-to-report + screenshot context; Sentry handles thrown errors.
25
+ ```
26
+
27
+ See [`@mushi-mushi/web` Sentry bridge](https://github.com/kensaurus/mushi-mushi/tree/master/packages/web#sentry-companion) for the dual-capture pattern on web.
28
+
15
29
  ## Quick start
16
30
 
17
31
  ```tsx
@@ -168,4 +182,4 @@ MIT
168
182
  <!-- mushi-readme-stats-footer -->
169
183
  ---
170
184
 
171
- <sub>Monorepo scale (June 2026): 47 edge functions · 256 SQL migrations · 13 outbound plugins · 11 inbound adapters · 18 pipeline agents. Canonical counts: <a href="https://github.com/kensaurus/mushi-mushi/blob/master/docs/stats.md">docs/stats.md</a> · <code>pnpm docs-stats</code></sub>
185
+ <sub>Monorepo scale (July 2026): 51 edge functions · 323 SQL migrations · 13 outbound plugins · 11 inbound adapters · 19 pipeline agents. Canonical counts: <a href="https://github.com/kensaurus/mushi-mushi/blob/master/docs/stats.md">docs/stats.md</a> · <code>pnpm docs-stats</code></sub>
package/SECURITY.md CHANGED
@@ -111,7 +111,7 @@ What we treat as in-scope attacker capabilities, and what we don't.
111
111
  | User pasting a Stripe / OpenAI / GitHub PAT into a bug report | ✅ | PII scrubber redacts ~15 vendor token formats client-side before the report leaves the device. Mirrors `packages/core/src/pii-scrubber.ts` across iOS, Android, Flutter, React Native. |
112
112
  | Stolen end-user device with the SDK installed | ⚠️ partial | Offline queue is AsyncStorage / Keychain / SharedPreferences — no app-level encryption beyond the OS default. Reports waiting to flush are vulnerable to a forensic attacker. |
113
113
  | Compromised Supabase service-role key | ❌ | Treated as a tier-0 incident; would require key rotation and audit-log forensics. Not defendable in software. |
114
- | Compromise of `kensaurus@gmail.com` | ❌ | Treated as a project-fork event; downstream consumers should pin to the last known-good version and follow the new release channel. |
114
+ | Compromise of `kensaurus@gmail.com` mail account | ❌ | Treated as a project-fork event; downstream consumers should pin to the last known-good version and follow the new release channel. |
115
115
  | Physical / OS-level attacker on an end-user device | ❌ | Out of scope. |
116
116
  | Malicious fork using the Mushi name to ship malware | ❌ (technical) ✅ (legal) | The MIT / AGPLv3 grant lets the fork exist; the trademark policy (`TRADEMARK.md`) makes shipping it under the Mushi name an infringement we will pursue. |
117
117
 
@@ -157,6 +157,29 @@ Structured fields you set explicitly (`metadata.userEmail`,
157
157
  opt-in attribution data, and silently rewriting them would break
158
158
  support workflows.
159
159
 
160
+ ### CLI funnel telemetry
161
+
162
+ The Mushi CLI sends two fire-and-forget signals to `/v1/cli/funnel` on your
163
+ project's endpoint to help us detect where the onboarding wizard drops off:
164
+
165
+ | Signal | When | What is sent |
166
+ |--------|------|-------------|
167
+ | `wizard_env_written` | After `npx mushi-mushi` writes `.env.local` | `event`, `source: "cli"` |
168
+ | `mcp_setup_done` | After `mushi setup --ide` verifies the MCP key | `event`, `source: "cli"` |
169
+
170
+ No file contents, stack traces, or identifiers beyond your project ID and API
171
+ key are sent. The key is already on the endpoint — this adds no new disclosure.
172
+
173
+ **Opt out** by setting `MUSHI_NO_TELEMETRY=1` in your shell or CI environment:
174
+
175
+ ```bash
176
+ MUSHI_NO_TELEMETRY=1 npx mushi-mushi
177
+ MUSHI_NO_TELEMETRY=1 mushi setup --ide cursor
178
+ ```
179
+
180
+ These signals never block or delay the wizard — they time out silently after
181
+ 4 seconds and errors are discarded.
182
+
160
183
  ### Where data lives
161
184
 
162
185
  - **Reports & telemetry** — Supabase Postgres in the `us-west-1` region.
package/dist/index.cjs CHANGED
@@ -543,6 +543,7 @@ var MushiBottomSheet = ({
543
543
  const [category, setCategory] = (0, import_react.useState)(null);
544
544
  const [description, setDescription] = (0, import_react.useState)("");
545
545
  const [phase, setPhase] = (0, import_react.useState)("form");
546
+ const [submitError, setSubmitError] = (0, import_react.useState)(null);
546
547
  const [sheetTab, setSheetTab] = (0, import_react.useState)("report");
547
548
  const [inboxReports, setInboxReports] = (0, import_react.useState)([]);
548
549
  const [inboxLoading, setInboxLoading] = (0, import_react.useState)(false);
@@ -616,6 +617,8 @@ var MushiBottomSheet = ({
616
617
  ...prev,
617
618
  { role: "bot", text: botText, options: reply.options }
618
619
  ]);
620
+ } catch {
621
+ setAssistantError("Could not reach the assistant. Try again.");
619
622
  } finally {
620
623
  setAssistantSending(false);
621
624
  }
@@ -689,24 +692,46 @@ var MushiBottomSheet = ({
689
692
  ).current;
690
693
  const handleSubmit = async () => {
691
694
  if (!category || !description.trim() || !mushi) return;
695
+ const trimmed = description.trim();
696
+ if (trimmed.length < 20) {
697
+ setSubmitError("Please write at least 20 characters so we can understand the issue.");
698
+ return;
699
+ }
700
+ setSubmitError(null);
692
701
  setPhase("sending");
693
702
  try {
694
- await mushi.submitReport({
703
+ const outcome = await mushi.submitReport({
695
704
  category,
696
- description: description.trim(),
705
+ description: trimmed,
697
706
  screenshotDataUrl: screenshotDataUrl && screenshotAttached ? screenshotDataUrl : void 0
698
707
  });
708
+ if (!outcome.ok) {
709
+ if (outcome.failureKind === "credentials" || outcome.failureKind === "quota") {
710
+ setSubmitError("Could not send \u2014 check the project API key or plan quota.");
711
+ setPhase("error");
712
+ return;
713
+ }
714
+ if (outcome.failureKind === "rate_limited") {
715
+ setSubmitError("Sending too fast \u2014 we queued this and will retry shortly.");
716
+ setPhase("error");
717
+ return;
718
+ }
719
+ setSubmitError("Queued for retry \u2014 we will send it when the connection is back.");
720
+ setPhase("error");
721
+ return;
722
+ }
699
723
  setPhase("sent");
700
724
  setTimeout(handleClose, 1400);
701
725
  } catch {
702
- setPhase("form");
726
+ setSubmitError("Something went wrong. Please try again.");
727
+ setPhase("error");
703
728
  }
704
729
  };
705
730
  const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
706
731
  const pal = (0, import_core3.mushiPalette)(dark ? "dark" : "light");
707
- const colors = dark ? { bg: pal.paper, text: pal.ink, sub: pal.inkMuted, card: pal.paperRaised, accent: import_core3.MUSHI_BANNER_NEON.bg, accentInk: import_core3.MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.6)", disabled: "#3a3a3c", disabledText: pal.inkFaint } : { bg: pal.paperRaised, text: pal.ink, sub: pal.inkMuted, card: pal.paper, accent: import_core3.MUSHI_BANNER_NEON.bg, accentInk: import_core3.MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.35)", disabled: "#d1d1d6", disabledText: pal.inkFaint };
732
+ const colors = dark ? { bg: pal.paper, text: pal.ink, sub: pal.inkMuted, card: pal.paperRaised, accent: import_core3.MUSHI_BANNER_NEON.bg, accentInk: import_core3.MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.6)", disabled: import_core3.MUSHI_CONTROL_DISABLED.dark, disabledText: pal.inkFaint } : { bg: pal.paperRaised, text: pal.ink, sub: pal.inkMuted, card: pal.paper, accent: import_core3.MUSHI_BANNER_NEON.bg, accentInk: import_core3.MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.35)", disabled: import_core3.MUSHI_CONTROL_DISABLED.light, disabledText: pal.inkFaint };
708
733
  const sheetTabs = ["report", "inbox", ...assistantEnabled ? ["assistant"] : []];
709
- const canSubmit = !!category && description.trim().length > 0 && phase === "form";
734
+ const canSubmit = !!category && description.trim().length >= 20 && (phase === "form" || phase === "error");
710
735
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
711
736
  import_react_native2.KeyboardAvoidingView,
712
737
  {
@@ -734,7 +759,7 @@ var MushiBottomSheet = ({
734
759
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.brandEyebrow, { color: import_core3.MUSHI_BANNER_NEON.fg }], children: "MUSHI \xB7 BETA" }),
735
760
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: [s.brandTitle, { color: import_core3.MUSHI_BANNER_NEON.fg }], children: sheetTab === "assistant" ? assistantLabel : sheetTab === "inbox" ? t.flows.reports.title : t.widget.title })
736
761
  ] }),
737
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: s.tabRow, children: sheetTabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
762
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [s.tabRow, { borderBottomColor: colors.border }], children: sheetTabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
738
763
  import_react_native2.TouchableOpacity,
739
764
  {
740
765
  onPress: () => {
@@ -887,16 +912,22 @@ var MushiBottomSheet = ({
887
912
  multiline: true,
888
913
  textAlignVertical: "top",
889
914
  value: description,
890
- onChangeText: setDescription,
891
- editable: phase === "form"
915
+ onChangeText: (text) => {
916
+ setDescription(text.slice(0, 4e3));
917
+ if (submitError) setSubmitError(null);
918
+ if (phase === "error") setPhase("form");
919
+ },
920
+ editable: phase === "form" || phase === "error",
921
+ maxLength: 4e3
892
922
  }
893
923
  ),
924
+ submitError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: { color: colors.accent, marginTop: 6, fontSize: 12 }, children: submitError }) : null,
894
925
  activeScreenshot ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: s.screenshotRow, children: [
895
926
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
896
927
  import_react_native2.Image,
897
928
  {
898
929
  source: { uri: activeScreenshot },
899
- style: s.screenshotThumb,
930
+ style: [s.screenshotThumb, { backgroundColor: colors.text }],
900
931
  accessibilityLabel: t.step3.screenshotPreviewAlt
901
932
  }
902
933
  ),
@@ -957,10 +988,10 @@ var s = import_react_native2.StyleSheet.create({
957
988
  left: 0,
958
989
  right: 0,
959
990
  minHeight: SHEET_HEIGHT,
960
- borderTopLeftRadius: 20,
961
- borderTopRightRadius: 20,
962
- paddingBottom: import_react_native2.Platform.OS === "ios" ? 34 : 16,
963
- shadowColor: "#000",
991
+ borderTopLeftRadius: import_core3.MUSHI_RADIUS.sheet,
992
+ borderTopRightRadius: import_core3.MUSHI_RADIUS.sheet,
993
+ paddingBottom: import_react_native2.Platform.OS === "ios" ? 34 : import_core3.MUSHI_SPACING.roomy,
994
+ shadowColor: import_core3.MUSHI_SHADOW_INK,
964
995
  shadowOffset: { width: 0, height: -3 },
965
996
  shadowOpacity: 0.12,
966
997
  shadowRadius: 8,
@@ -968,22 +999,22 @@ var s = import_react_native2.StyleSheet.create({
968
999
  },
969
1000
  handleArea: {
970
1001
  alignItems: "center",
971
- paddingVertical: 10
1002
+ paddingVertical: import_core3.MUSHI_SPACING.comfy
972
1003
  },
973
1004
  handle: {
974
1005
  width: 40,
975
1006
  height: 5,
976
- borderRadius: 3,
1007
+ borderRadius: import_core3.MUSHI_RADIUS.control,
977
1008
  opacity: 0.5
978
1009
  },
979
1010
  brandHeader: {
980
- paddingHorizontal: 20,
981
- paddingTop: 10,
982
- paddingBottom: 12,
1011
+ paddingHorizontal: import_core3.MUSHI_SPACING.lounge,
1012
+ paddingTop: import_core3.MUSHI_SPACING.comfy,
1013
+ paddingBottom: import_core3.MUSHI_SPACING.comfy,
983
1014
  borderBottomWidth: 1.5
984
1015
  },
985
1016
  brandEyebrow: {
986
- fontSize: 10,
1017
+ fontSize: import_core3.MUSHI_TYPE.sizeLabel,
987
1018
  fontWeight: "700",
988
1019
  letterSpacing: 1.2,
989
1020
  marginBottom: 2,
@@ -995,59 +1026,61 @@ var s = import_react_native2.StyleSheet.create({
995
1026
  letterSpacing: -0.2
996
1027
  },
997
1028
  body: {
998
- paddingHorizontal: 20,
999
- paddingTop: 4
1029
+ paddingHorizontal: import_core3.MUSHI_SPACING.lounge,
1030
+ paddingTop: import_core3.MUSHI_SPACING.tight
1000
1031
  },
1001
1032
  title: {
1002
1033
  fontSize: 20,
1003
1034
  fontWeight: "700",
1004
- marginBottom: 16
1035
+ marginBottom: import_core3.MUSHI_SPACING.roomy
1005
1036
  },
1006
1037
  stepLabel: {
1007
- fontSize: 12,
1038
+ fontSize: import_core3.MUSHI_TYPE.sizeLabel,
1008
1039
  fontWeight: "600",
1009
1040
  letterSpacing: 0.2,
1010
- marginTop: 8,
1011
- marginBottom: 10,
1041
+ marginTop: import_core3.MUSHI_SPACING.snug,
1042
+ marginBottom: import_core3.MUSHI_SPACING.comfy,
1012
1043
  textTransform: "uppercase"
1013
1044
  },
1014
1045
  catRow: {
1015
1046
  flexDirection: "row",
1016
1047
  justifyContent: "space-between",
1017
- marginBottom: 16
1048
+ marginBottom: import_core3.MUSHI_SPACING.roomy
1018
1049
  },
1019
1050
  catBtn: {
1020
1051
  alignItems: "center",
1021
- paddingVertical: 10,
1052
+ paddingVertical: import_core3.MUSHI_SPACING.comfy,
1022
1053
  paddingHorizontal: 6,
1023
- borderRadius: 12,
1054
+ borderRadius: import_core3.MUSHI_RADIUS.card,
1024
1055
  borderWidth: 1,
1025
1056
  flex: 1,
1026
- marginHorizontal: 3
1057
+ marginHorizontal: 3,
1058
+ minHeight: 44
1027
1059
  },
1028
1060
  catEmoji: {
1029
1061
  fontSize: 22,
1030
- marginBottom: 4
1062
+ marginBottom: import_core3.MUSHI_SPACING.tight
1031
1063
  },
1032
1064
  catLabel: {
1033
- fontSize: 11,
1065
+ fontSize: import_core3.MUSHI_TYPE.sizeLabel,
1034
1066
  fontWeight: "600"
1035
1067
  },
1036
1068
  input: {
1037
1069
  borderWidth: 1,
1038
- borderRadius: 12,
1070
+ borderRadius: import_core3.MUSHI_RADIUS.card,
1039
1071
  padding: 14,
1040
1072
  fontSize: 15,
1041
1073
  minHeight: 100,
1042
- marginBottom: 16
1074
+ marginBottom: import_core3.MUSHI_SPACING.roomy
1043
1075
  },
1044
1076
  submitBtn: {
1045
- borderRadius: 12,
1077
+ borderRadius: import_core3.MUSHI_RADIUS.card,
1046
1078
  paddingVertical: 14,
1047
- alignItems: "center"
1079
+ alignItems: "center",
1080
+ minHeight: 44
1048
1081
  },
1049
1082
  submitText: {
1050
- color: "#fff",
1083
+ color: import_core3.MUSHI_INVERSE,
1051
1084
  fontSize: 16,
1052
1085
  fontWeight: "700"
1053
1086
  },
@@ -1078,7 +1111,6 @@ var s = import_react_native2.StyleSheet.create({
1078
1111
  width: 48,
1079
1112
  height: 36,
1080
1113
  borderRadius: 4,
1081
- backgroundColor: "#000",
1082
1114
  flexShrink: 0
1083
1115
  },
1084
1116
  screenshotMeta: {
@@ -1100,7 +1132,6 @@ var s = import_react_native2.StyleSheet.create({
1100
1132
  tabRow: {
1101
1133
  flexDirection: "row",
1102
1134
  borderBottomWidth: import_react_native2.StyleSheet.hairlineWidth,
1103
- borderBottomColor: "#ccc",
1104
1135
  marginBottom: 8
1105
1136
  },
1106
1137
  tabBtn: {
@@ -1212,7 +1243,8 @@ var MushiFloatingButton = ({
1212
1243
  const currentX = (0, import_react2.useRef)(baseX);
1213
1244
  const currentY = (0, import_react2.useRef)(0);
1214
1245
  const isDragging = (0, import_react2.useRef)(false);
1215
- const accentColor = accent ?? (0, import_core4.mushiPalette)(dark ? "dark" : "light").accent;
1246
+ const pal = (0, import_core4.mushiPalette)(dark ? "dark" : "light");
1247
+ const accentColor = accent ?? pal.accent;
1216
1248
  (0, import_react2.useEffect)(() => {
1217
1249
  if (!persist) return;
1218
1250
  void loadStoredPos().then((pos) => {
@@ -1315,7 +1347,7 @@ var MushiFloatingButton = ({
1315
1347
  left: posX,
1316
1348
  bottom: import_react_native3.Animated.add(new import_react_native3.Animated.Value(baseBottom), import_react_native3.Animated.multiply(posY, -1)),
1317
1349
  backgroundColor: accentColor,
1318
- shadowColor: dark ? accentColor : "#000",
1350
+ shadowColor: dark ? accentColor : pal.ink,
1319
1351
  transform: [{ scale }]
1320
1352
  }
1321
1353
  ],
@@ -1404,7 +1436,7 @@ var s3 = import_react_native4.StyleSheet.create({
1404
1436
  backgroundColor: import_core5.MUSHI_BANNER_NEON.bg,
1405
1437
  borderTopWidth: 1,
1406
1438
  borderTopColor: import_core5.MUSHI_BANNER_NEON.border,
1407
- zIndex: 99998,
1439
+ zIndex: import_core5.MUSHI_Z.banner,
1408
1440
  elevation: 8,
1409
1441
  paddingBottom: import_react_native4.Platform.OS === "ios" ? 0 : 0
1410
1442
  },
@@ -1412,12 +1444,13 @@ var s3 = import_react_native4.StyleSheet.create({
1412
1444
  flex: 1,
1413
1445
  flexDirection: "row",
1414
1446
  alignItems: "center",
1415
- gap: 8,
1416
- paddingHorizontal: 12,
1417
- paddingVertical: 8
1447
+ gap: import_core5.MUSHI_SPACING.snug,
1448
+ paddingHorizontal: import_core5.MUSHI_SPACING.comfy,
1449
+ paddingVertical: import_core5.MUSHI_SPACING.snug,
1450
+ minHeight: 44
1418
1451
  },
1419
1452
  eyebrow: {
1420
- fontSize: 10,
1453
+ fontSize: import_core5.MUSHI_TYPE.sizeLabel,
1421
1454
  fontWeight: "700",
1422
1455
  letterSpacing: 1.2,
1423
1456
  color: import_core5.MUSHI_BANNER_NEON.fg
@@ -1448,7 +1481,7 @@ var s3 = import_react_native4.StyleSheet.create({
1448
1481
 
1449
1482
  // src/version.ts
1450
1483
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
1451
- var MUSHI_SDK_VERSION = "0.20.0" ? "0.20.0" : "0.17.0";
1484
+ var MUSHI_SDK_VERSION = "0.21.0" ? "0.21.0" : "0.17.0";
1452
1485
 
1453
1486
  // src/provider.tsx
1454
1487
  var import_jsx_runtime4 = require("react/jsx-runtime");
@@ -1738,21 +1771,34 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1738
1771
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1739
1772
  };
1740
1773
  const client = apiClientRef.current;
1741
- if (!client) return;
1774
+ if (!client) {
1775
+ console.error(
1776
+ "[Mushi] submitReport called before the SDK finished initializing. Ensure <MushiProvider> is mounted with a valid projectId and apiKey."
1777
+ );
1778
+ return { ok: false, failureKind: "permanent" };
1779
+ }
1742
1780
  const result = await client.submitReport(report);
1743
1781
  if (!result.ok) {
1744
- const isCredentialError = result.error?.code === "HTTP_401" || result.error?.code === "HTTP_403" || result.error?.code?.includes("UNAUTHORIZED") || result.error?.code?.includes("FORBIDDEN");
1782
+ const code = result.error?.code ?? "";
1783
+ const isCredentialError = code === "HTTP_401" || code === "HTTP_403" || code.includes("UNAUTHORIZED") || code.includes("FORBIDDEN") || code.includes("INVALID_API_KEY");
1745
1784
  if (isCredentialError) {
1746
1785
  const where = apiEndpoint.replace(/\/$/, "") === import_core6.DEFAULT_API_ENDPOINT ? "Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects" : "Get the correct values from your Mushi console's Projects page.";
1747
1786
  console.error(
1748
1787
  '[Mushi] Credentials rejected. Check your projectId and apiKey (must have "report:write" scope). ' + where
1749
1788
  );
1750
- return;
1789
+ return { ok: false, failureKind: "credentials" };
1751
1790
  }
1791
+ const isRateLimited = code === "HTTP_429" || code.includes("RATE_LIMIT");
1752
1792
  await queueRef.current?.enqueue(report);
1753
1793
  config.callbacks?.onReportQueued?.(report.id);
1794
+ return {
1795
+ ok: false,
1796
+ queued: true,
1797
+ failureKind: isRateLimited ? "rate_limited" : "retrying"
1798
+ };
1754
1799
  } else {
1755
1800
  config.callbacks?.onReportSubmitted?.(report.id);
1801
+ return { ok: true };
1756
1802
  }
1757
1803
  },
1758
1804
  [config.projectId, config.callbacks, apiEndpoint]
package/dist/index.d.cts CHANGED
@@ -128,7 +128,11 @@ interface MushiRNInstance {
128
128
  description: string;
129
129
  category: string;
130
130
  screenshotDataUrl?: string;
131
- }): Promise<void>;
131
+ }): Promise<{
132
+ ok: boolean;
133
+ queued?: boolean;
134
+ failureKind?: 'offline' | 'retrying' | 'rate_limited' | 'quota' | 'permanent' | 'credentials';
135
+ }>;
132
136
  getDeviceInfo(): ReturnType<typeof getDeviceInfo>;
133
137
  getConsoleEntries(): ReturnType<ReturnType<typeof setupConsoleCapture>['getEntries']>;
134
138
  getNetworkEntries(): ReturnType<ReturnType<typeof setupNetworkCapture>['getEntries']>;
package/dist/index.d.ts CHANGED
@@ -128,7 +128,11 @@ interface MushiRNInstance {
128
128
  description: string;
129
129
  category: string;
130
130
  screenshotDataUrl?: string;
131
- }): Promise<void>;
131
+ }): Promise<{
132
+ ok: boolean;
133
+ queued?: boolean;
134
+ failureKind?: 'offline' | 'retrying' | 'rate_limited' | 'quota' | 'permanent' | 'credentials';
135
+ }>;
132
136
  getDeviceInfo(): ReturnType<typeof getDeviceInfo>;
133
137
  getConsoleEntries(): ReturnType<ReturnType<typeof setupConsoleCapture>['getEntries']>;
134
138
  getNetworkEntries(): ReturnType<ReturnType<typeof setupNetworkCapture>['getEntries']>;
package/dist/index.js CHANGED
@@ -474,7 +474,17 @@ import {
474
474
  ScrollView,
475
475
  useColorScheme
476
476
  } from "react-native";
477
- import { mushiPalette, MUSHI_BANNER_NEON, MUSHI_COPY } from "@mushi-mushi/core";
477
+ import {
478
+ mushiPalette,
479
+ MUSHI_BANNER_NEON,
480
+ MUSHI_COPY,
481
+ MUSHI_CONTROL_DISABLED,
482
+ MUSHI_INVERSE,
483
+ MUSHI_RADIUS,
484
+ MUSHI_SHADOW_INK,
485
+ MUSHI_SPACING,
486
+ MUSHI_TYPE
487
+ } from "@mushi-mushi/core";
478
488
  import { getLocale } from "@mushi-mushi/web/i18n";
479
489
 
480
490
  // src/reporter-status.ts
@@ -540,6 +550,7 @@ var MushiBottomSheet = ({
540
550
  const [category, setCategory] = useState(null);
541
551
  const [description, setDescription] = useState("");
542
552
  const [phase, setPhase] = useState("form");
553
+ const [submitError, setSubmitError] = useState(null);
543
554
  const [sheetTab, setSheetTab] = useState("report");
544
555
  const [inboxReports, setInboxReports] = useState([]);
545
556
  const [inboxLoading, setInboxLoading] = useState(false);
@@ -613,6 +624,8 @@ var MushiBottomSheet = ({
613
624
  ...prev,
614
625
  { role: "bot", text: botText, options: reply.options }
615
626
  ]);
627
+ } catch {
628
+ setAssistantError("Could not reach the assistant. Try again.");
616
629
  } finally {
617
630
  setAssistantSending(false);
618
631
  }
@@ -686,24 +699,46 @@ var MushiBottomSheet = ({
686
699
  ).current;
687
700
  const handleSubmit = async () => {
688
701
  if (!category || !description.trim() || !mushi) return;
702
+ const trimmed = description.trim();
703
+ if (trimmed.length < 20) {
704
+ setSubmitError("Please write at least 20 characters so we can understand the issue.");
705
+ return;
706
+ }
707
+ setSubmitError(null);
689
708
  setPhase("sending");
690
709
  try {
691
- await mushi.submitReport({
710
+ const outcome = await mushi.submitReport({
692
711
  category,
693
- description: description.trim(),
712
+ description: trimmed,
694
713
  screenshotDataUrl: screenshotDataUrl && screenshotAttached ? screenshotDataUrl : void 0
695
714
  });
715
+ if (!outcome.ok) {
716
+ if (outcome.failureKind === "credentials" || outcome.failureKind === "quota") {
717
+ setSubmitError("Could not send \u2014 check the project API key or plan quota.");
718
+ setPhase("error");
719
+ return;
720
+ }
721
+ if (outcome.failureKind === "rate_limited") {
722
+ setSubmitError("Sending too fast \u2014 we queued this and will retry shortly.");
723
+ setPhase("error");
724
+ return;
725
+ }
726
+ setSubmitError("Queued for retry \u2014 we will send it when the connection is back.");
727
+ setPhase("error");
728
+ return;
729
+ }
696
730
  setPhase("sent");
697
731
  setTimeout(handleClose, 1400);
698
732
  } catch {
699
- setPhase("form");
733
+ setSubmitError("Something went wrong. Please try again.");
734
+ setPhase("error");
700
735
  }
701
736
  };
702
737
  const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
703
738
  const pal = mushiPalette(dark ? "dark" : "light");
704
- const colors = dark ? { bg: pal.paper, text: pal.ink, sub: pal.inkMuted, card: pal.paperRaised, accent: MUSHI_BANNER_NEON.bg, accentInk: MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.6)", disabled: "#3a3a3c", disabledText: pal.inkFaint } : { bg: pal.paperRaised, text: pal.ink, sub: pal.inkMuted, card: pal.paper, accent: MUSHI_BANNER_NEON.bg, accentInk: MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.35)", disabled: "#d1d1d6", disabledText: pal.inkFaint };
739
+ const colors = dark ? { bg: pal.paper, text: pal.ink, sub: pal.inkMuted, card: pal.paperRaised, accent: MUSHI_BANNER_NEON.bg, accentInk: MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.6)", disabled: MUSHI_CONTROL_DISABLED.dark, disabledText: pal.inkFaint } : { bg: pal.paperRaised, text: pal.ink, sub: pal.inkMuted, card: pal.paper, accent: MUSHI_BANNER_NEON.bg, accentInk: MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.35)", disabled: MUSHI_CONTROL_DISABLED.light, disabledText: pal.inkFaint };
705
740
  const sheetTabs = ["report", "inbox", ...assistantEnabled ? ["assistant"] : []];
706
- const canSubmit = !!category && description.trim().length > 0 && phase === "form";
741
+ const canSubmit = !!category && description.trim().length >= 20 && (phase === "form" || phase === "error");
707
742
  return /* @__PURE__ */ jsx(Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ jsxs(
708
743
  KeyboardAvoidingView,
709
744
  {
@@ -731,7 +766,7 @@ var MushiBottomSheet = ({
731
766
  /* @__PURE__ */ jsx(Text, { style: [s.brandEyebrow, { color: MUSHI_BANNER_NEON.fg }], children: "MUSHI \xB7 BETA" }),
732
767
  /* @__PURE__ */ jsx(Text, { style: [s.brandTitle, { color: MUSHI_BANNER_NEON.fg }], children: sheetTab === "assistant" ? assistantLabel : sheetTab === "inbox" ? t.flows.reports.title : t.widget.title })
733
768
  ] }),
734
- /* @__PURE__ */ jsx(View, { style: s.tabRow, children: sheetTabs.map((tab) => /* @__PURE__ */ jsx(
769
+ /* @__PURE__ */ jsx(View, { style: [s.tabRow, { borderBottomColor: colors.border }], children: sheetTabs.map((tab) => /* @__PURE__ */ jsx(
735
770
  TouchableOpacity,
736
771
  {
737
772
  onPress: () => {
@@ -884,16 +919,22 @@ var MushiBottomSheet = ({
884
919
  multiline: true,
885
920
  textAlignVertical: "top",
886
921
  value: description,
887
- onChangeText: setDescription,
888
- editable: phase === "form"
922
+ onChangeText: (text) => {
923
+ setDescription(text.slice(0, 4e3));
924
+ if (submitError) setSubmitError(null);
925
+ if (phase === "error") setPhase("form");
926
+ },
927
+ editable: phase === "form" || phase === "error",
928
+ maxLength: 4e3
889
929
  }
890
930
  ),
931
+ submitError ? /* @__PURE__ */ jsx(Text, { style: { color: colors.accent, marginTop: 6, fontSize: 12 }, children: submitError }) : null,
891
932
  activeScreenshot ? /* @__PURE__ */ jsxs(View, { style: s.screenshotRow, children: [
892
933
  /* @__PURE__ */ jsx(
893
934
  Image,
894
935
  {
895
936
  source: { uri: activeScreenshot },
896
- style: s.screenshotThumb,
937
+ style: [s.screenshotThumb, { backgroundColor: colors.text }],
897
938
  accessibilityLabel: t.step3.screenshotPreviewAlt
898
939
  }
899
940
  ),
@@ -954,10 +995,10 @@ var s = StyleSheet.create({
954
995
  left: 0,
955
996
  right: 0,
956
997
  minHeight: SHEET_HEIGHT,
957
- borderTopLeftRadius: 20,
958
- borderTopRightRadius: 20,
959
- paddingBottom: Platform2.OS === "ios" ? 34 : 16,
960
- shadowColor: "#000",
998
+ borderTopLeftRadius: MUSHI_RADIUS.sheet,
999
+ borderTopRightRadius: MUSHI_RADIUS.sheet,
1000
+ paddingBottom: Platform2.OS === "ios" ? 34 : MUSHI_SPACING.roomy,
1001
+ shadowColor: MUSHI_SHADOW_INK,
961
1002
  shadowOffset: { width: 0, height: -3 },
962
1003
  shadowOpacity: 0.12,
963
1004
  shadowRadius: 8,
@@ -965,22 +1006,22 @@ var s = StyleSheet.create({
965
1006
  },
966
1007
  handleArea: {
967
1008
  alignItems: "center",
968
- paddingVertical: 10
1009
+ paddingVertical: MUSHI_SPACING.comfy
969
1010
  },
970
1011
  handle: {
971
1012
  width: 40,
972
1013
  height: 5,
973
- borderRadius: 3,
1014
+ borderRadius: MUSHI_RADIUS.control,
974
1015
  opacity: 0.5
975
1016
  },
976
1017
  brandHeader: {
977
- paddingHorizontal: 20,
978
- paddingTop: 10,
979
- paddingBottom: 12,
1018
+ paddingHorizontal: MUSHI_SPACING.lounge,
1019
+ paddingTop: MUSHI_SPACING.comfy,
1020
+ paddingBottom: MUSHI_SPACING.comfy,
980
1021
  borderBottomWidth: 1.5
981
1022
  },
982
1023
  brandEyebrow: {
983
- fontSize: 10,
1024
+ fontSize: MUSHI_TYPE.sizeLabel,
984
1025
  fontWeight: "700",
985
1026
  letterSpacing: 1.2,
986
1027
  marginBottom: 2,
@@ -992,59 +1033,61 @@ var s = StyleSheet.create({
992
1033
  letterSpacing: -0.2
993
1034
  },
994
1035
  body: {
995
- paddingHorizontal: 20,
996
- paddingTop: 4
1036
+ paddingHorizontal: MUSHI_SPACING.lounge,
1037
+ paddingTop: MUSHI_SPACING.tight
997
1038
  },
998
1039
  title: {
999
1040
  fontSize: 20,
1000
1041
  fontWeight: "700",
1001
- marginBottom: 16
1042
+ marginBottom: MUSHI_SPACING.roomy
1002
1043
  },
1003
1044
  stepLabel: {
1004
- fontSize: 12,
1045
+ fontSize: MUSHI_TYPE.sizeLabel,
1005
1046
  fontWeight: "600",
1006
1047
  letterSpacing: 0.2,
1007
- marginTop: 8,
1008
- marginBottom: 10,
1048
+ marginTop: MUSHI_SPACING.snug,
1049
+ marginBottom: MUSHI_SPACING.comfy,
1009
1050
  textTransform: "uppercase"
1010
1051
  },
1011
1052
  catRow: {
1012
1053
  flexDirection: "row",
1013
1054
  justifyContent: "space-between",
1014
- marginBottom: 16
1055
+ marginBottom: MUSHI_SPACING.roomy
1015
1056
  },
1016
1057
  catBtn: {
1017
1058
  alignItems: "center",
1018
- paddingVertical: 10,
1059
+ paddingVertical: MUSHI_SPACING.comfy,
1019
1060
  paddingHorizontal: 6,
1020
- borderRadius: 12,
1061
+ borderRadius: MUSHI_RADIUS.card,
1021
1062
  borderWidth: 1,
1022
1063
  flex: 1,
1023
- marginHorizontal: 3
1064
+ marginHorizontal: 3,
1065
+ minHeight: 44
1024
1066
  },
1025
1067
  catEmoji: {
1026
1068
  fontSize: 22,
1027
- marginBottom: 4
1069
+ marginBottom: MUSHI_SPACING.tight
1028
1070
  },
1029
1071
  catLabel: {
1030
- fontSize: 11,
1072
+ fontSize: MUSHI_TYPE.sizeLabel,
1031
1073
  fontWeight: "600"
1032
1074
  },
1033
1075
  input: {
1034
1076
  borderWidth: 1,
1035
- borderRadius: 12,
1077
+ borderRadius: MUSHI_RADIUS.card,
1036
1078
  padding: 14,
1037
1079
  fontSize: 15,
1038
1080
  minHeight: 100,
1039
- marginBottom: 16
1081
+ marginBottom: MUSHI_SPACING.roomy
1040
1082
  },
1041
1083
  submitBtn: {
1042
- borderRadius: 12,
1084
+ borderRadius: MUSHI_RADIUS.card,
1043
1085
  paddingVertical: 14,
1044
- alignItems: "center"
1086
+ alignItems: "center",
1087
+ minHeight: 44
1045
1088
  },
1046
1089
  submitText: {
1047
- color: "#fff",
1090
+ color: MUSHI_INVERSE,
1048
1091
  fontSize: 16,
1049
1092
  fontWeight: "700"
1050
1093
  },
@@ -1075,7 +1118,6 @@ var s = StyleSheet.create({
1075
1118
  width: 48,
1076
1119
  height: 36,
1077
1120
  borderRadius: 4,
1078
- backgroundColor: "#000",
1079
1121
  flexShrink: 0
1080
1122
  },
1081
1123
  screenshotMeta: {
@@ -1097,7 +1139,6 @@ var s = StyleSheet.create({
1097
1139
  tabRow: {
1098
1140
  flexDirection: "row",
1099
1141
  borderBottomWidth: StyleSheet.hairlineWidth,
1100
- borderBottomColor: "#ccc",
1101
1142
  marginBottom: 8
1102
1143
  },
1103
1144
  tabBtn: {
@@ -1216,7 +1257,8 @@ var MushiFloatingButton = ({
1216
1257
  const currentX = useRef2(baseX);
1217
1258
  const currentY = useRef2(0);
1218
1259
  const isDragging = useRef2(false);
1219
- const accentColor = accent ?? mushiPalette2(dark ? "dark" : "light").accent;
1260
+ const pal = mushiPalette2(dark ? "dark" : "light");
1261
+ const accentColor = accent ?? pal.accent;
1220
1262
  useEffect2(() => {
1221
1263
  if (!persist) return;
1222
1264
  void loadStoredPos().then((pos) => {
@@ -1319,7 +1361,7 @@ var MushiFloatingButton = ({
1319
1361
  left: posX,
1320
1362
  bottom: Animated2.add(new Animated2.Value(baseBottom), Animated2.multiply(posY, -1)),
1321
1363
  backgroundColor: accentColor,
1322
- shadowColor: dark ? accentColor : "#000",
1364
+ shadowColor: dark ? accentColor : pal.ink,
1323
1365
  transform: [{ scale }]
1324
1366
  }
1325
1367
  ],
@@ -1358,7 +1400,7 @@ import {
1358
1400
  StyleSheet as StyleSheet3,
1359
1401
  Platform as Platform4
1360
1402
  } from "react-native";
1361
- import { MUSHI_BANNER_NEON as MUSHI_BANNER_NEON2, MUSHI_COPY as MUSHI_COPY3, MUSHI_GEOMETRY as MUSHI_GEOMETRY2 } from "@mushi-mushi/core";
1403
+ import { MUSHI_BANNER_NEON as MUSHI_BANNER_NEON2, MUSHI_COPY as MUSHI_COPY3, MUSHI_GEOMETRY as MUSHI_GEOMETRY2, MUSHI_SPACING as MUSHI_SPACING2, MUSHI_TYPE as MUSHI_TYPE2, MUSHI_Z } from "@mushi-mushi/core";
1362
1404
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1363
1405
  var MUSHI_BANNER_DEFAULT_HEIGHT = MUSHI_GEOMETRY2.bannerHeight;
1364
1406
  var MushiBanner = ({
@@ -1414,7 +1456,7 @@ var s3 = StyleSheet3.create({
1414
1456
  backgroundColor: MUSHI_BANNER_NEON2.bg,
1415
1457
  borderTopWidth: 1,
1416
1458
  borderTopColor: MUSHI_BANNER_NEON2.border,
1417
- zIndex: 99998,
1459
+ zIndex: MUSHI_Z.banner,
1418
1460
  elevation: 8,
1419
1461
  paddingBottom: Platform4.OS === "ios" ? 0 : 0
1420
1462
  },
@@ -1422,12 +1464,13 @@ var s3 = StyleSheet3.create({
1422
1464
  flex: 1,
1423
1465
  flexDirection: "row",
1424
1466
  alignItems: "center",
1425
- gap: 8,
1426
- paddingHorizontal: 12,
1427
- paddingVertical: 8
1467
+ gap: MUSHI_SPACING2.snug,
1468
+ paddingHorizontal: MUSHI_SPACING2.comfy,
1469
+ paddingVertical: MUSHI_SPACING2.snug,
1470
+ minHeight: 44
1428
1471
  },
1429
1472
  eyebrow: {
1430
- fontSize: 10,
1473
+ fontSize: MUSHI_TYPE2.sizeLabel,
1431
1474
  fontWeight: "700",
1432
1475
  letterSpacing: 1.2,
1433
1476
  color: MUSHI_BANNER_NEON2.fg
@@ -1458,7 +1501,7 @@ var s3 = StyleSheet3.create({
1458
1501
 
1459
1502
  // src/version.ts
1460
1503
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
1461
- var MUSHI_SDK_VERSION = "0.20.0" ? "0.20.0" : "0.17.0";
1504
+ var MUSHI_SDK_VERSION = "0.21.0" ? "0.21.0" : "0.17.0";
1462
1505
 
1463
1506
  // src/provider.tsx
1464
1507
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -1748,21 +1791,34 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1748
1791
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1749
1792
  };
1750
1793
  const client = apiClientRef.current;
1751
- if (!client) return;
1794
+ if (!client) {
1795
+ console.error(
1796
+ "[Mushi] submitReport called before the SDK finished initializing. Ensure <MushiProvider> is mounted with a valid projectId and apiKey."
1797
+ );
1798
+ return { ok: false, failureKind: "permanent" };
1799
+ }
1752
1800
  const result = await client.submitReport(report);
1753
1801
  if (!result.ok) {
1754
- const isCredentialError = result.error?.code === "HTTP_401" || result.error?.code === "HTTP_403" || result.error?.code?.includes("UNAUTHORIZED") || result.error?.code?.includes("FORBIDDEN");
1802
+ const code = result.error?.code ?? "";
1803
+ const isCredentialError = code === "HTTP_401" || code === "HTTP_403" || code.includes("UNAUTHORIZED") || code.includes("FORBIDDEN") || code.includes("INVALID_API_KEY");
1755
1804
  if (isCredentialError) {
1756
1805
  const where = apiEndpoint.replace(/\/$/, "") === DEFAULT_API_ENDPOINT ? "Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects" : "Get the correct values from your Mushi console's Projects page.";
1757
1806
  console.error(
1758
1807
  '[Mushi] Credentials rejected. Check your projectId and apiKey (must have "report:write" scope). ' + where
1759
1808
  );
1760
- return;
1809
+ return { ok: false, failureKind: "credentials" };
1761
1810
  }
1811
+ const isRateLimited = code === "HTTP_429" || code.includes("RATE_LIMIT");
1762
1812
  await queueRef.current?.enqueue(report);
1763
1813
  config.callbacks?.onReportQueued?.(report.id);
1814
+ return {
1815
+ ok: false,
1816
+ queued: true,
1817
+ failureKind: isRateLimited ? "rate_limited" : "retrying"
1818
+ };
1764
1819
  } else {
1765
1820
  config.callbacks?.onReportSubmitted?.(report.id);
1821
+ return { ok: true };
1766
1822
  }
1767
1823
  },
1768
1824
  [config.projectId, config.callbacks, apiEndpoint]
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@mushi-mushi/react-native",
3
- "version": "0.20.0",
3
+ "version": "0.21.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.cjs",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
+ "size-limit": [
10
+ {
11
+ "path": "dist/index.js",
12
+ "limit": "22 KB"
13
+ }
14
+ ],
9
15
  "peerDependencies": {
10
16
  "@react-native-async-storage/async-storage": ">=1.19",
11
17
  "@react-native-community/netinfo": ">=11",
@@ -37,16 +43,21 @@
37
43
  }
38
44
  },
39
45
  "dependencies": {
40
- "@mushi-mushi/core": "^1.21.0",
41
- "@mushi-mushi/web": "^1.21.0"
46
+ "@mushi-mushi/core": "^1.26.0",
47
+ "@mushi-mushi/web": "^1.26.0"
42
48
  },
43
49
  "devDependencies": {
50
+ "@size-limit/file": "^12.1.0",
44
51
  "@types/react": "^19.2.17",
52
+ "eslint": "^10.6.0",
45
53
  "react": "^19.2.7",
46
54
  "react-native": "^0.86.0",
55
+ "size-limit": "^12.1.0",
47
56
  "tsup": "^8.5.1",
48
57
  "typescript": "^6.0.3",
49
- "vitest": "^4.1.8"
58
+ "vitest": "^4.1.9",
59
+ "@mushi-mushi/eslint-config": "0.0.0",
60
+ "eslint-plugin-mushi-mushi": "0.3.1"
50
61
  },
51
62
  "author": "Kenji Sakuramoto (https://bsky.app/profile/mushimushi.dev)",
52
63
  "repository": {
@@ -116,7 +127,9 @@
116
127
  },
117
128
  "scripts": {
118
129
  "build": "tsup",
130
+ "lint": "eslint src/",
119
131
  "test": "vitest run",
120
- "typecheck": "tsc --noEmit"
132
+ "typecheck": "tsc --noEmit",
133
+ "size": "size-limit"
121
134
  }
122
135
  }