@mushi-mushi/react-native 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1,5 +1,14 @@
1
1
  // src/provider.tsx
2
- import { createContext, useContext, useEffect, useRef } from "react";
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useEffect as useEffect2,
6
+ useRef as useRef3,
7
+ useState as useState2,
8
+ useCallback as useCallback3,
9
+ useMemo
10
+ } from "react";
11
+ import { createApiClient, DEFAULT_API_ENDPOINT } from "@mushi-mushi/core";
3
12
 
4
13
  // src/capture/console-capture.ts
5
14
  function setupConsoleCapture(maxEntries = 100) {
@@ -174,60 +183,493 @@ var AsyncStorageQueue = class {
174
183
  }
175
184
  };
176
185
 
186
+ // src/components/MushiBottomSheet.tsx
187
+ import {
188
+ useRef,
189
+ useState,
190
+ useEffect,
191
+ useCallback
192
+ } from "react";
193
+ import {
194
+ Modal,
195
+ View,
196
+ Text,
197
+ TextInput,
198
+ TouchableOpacity,
199
+ Animated,
200
+ PanResponder,
201
+ StyleSheet,
202
+ Dimensions as Dimensions2,
203
+ Platform as Platform2,
204
+ KeyboardAvoidingView,
205
+ useColorScheme
206
+ } from "react-native";
207
+ import { jsx, jsxs } from "react/jsx-runtime";
208
+ var { height: SCREEN_HEIGHT } = Dimensions2.get("window");
209
+ var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
210
+ var DISMISS_THRESHOLD = 80;
211
+ var CATEGORIES = [
212
+ { key: "bug", emoji: "\u{1F41B}", label: "Bug" },
213
+ { key: "slow", emoji: "\u{1F422}", label: "Slow" },
214
+ { key: "visual", emoji: "\u{1F3A8}", label: "Visual" },
215
+ { key: "confusing", emoji: "\u{1F615}", label: "Confusing" },
216
+ { key: "other", emoji: "\u{1F4AC}", label: "Other" }
217
+ ];
218
+ var MushiBottomSheet = ({ visible, onClose }) => {
219
+ const mushi = useMushiContext();
220
+ const scheme = useColorScheme();
221
+ const dark = scheme === "dark";
222
+ const translateY = useRef(new Animated.Value(SHEET_HEIGHT)).current;
223
+ const backdropOpacity = useRef(new Animated.Value(0)).current;
224
+ const [category, setCategory] = useState(null);
225
+ const [description, setDescription] = useState("");
226
+ const [phase, setPhase] = useState("form");
227
+ const resetForm = useCallback(() => {
228
+ setCategory(null);
229
+ setDescription("");
230
+ setPhase("form");
231
+ }, []);
232
+ const animateIn = useCallback(() => {
233
+ Animated.parallel([
234
+ Animated.spring(translateY, {
235
+ toValue: 0,
236
+ useNativeDriver: true,
237
+ damping: 20,
238
+ stiffness: 200
239
+ }),
240
+ Animated.timing(backdropOpacity, {
241
+ toValue: 1,
242
+ duration: 250,
243
+ useNativeDriver: true
244
+ })
245
+ ]).start();
246
+ }, [translateY, backdropOpacity]);
247
+ const animateOut = useCallback(
248
+ (cb) => {
249
+ Animated.parallel([
250
+ Animated.timing(translateY, {
251
+ toValue: SHEET_HEIGHT,
252
+ duration: 220,
253
+ useNativeDriver: true
254
+ }),
255
+ Animated.timing(backdropOpacity, {
256
+ toValue: 0,
257
+ duration: 200,
258
+ useNativeDriver: true
259
+ })
260
+ ]).start(() => {
261
+ resetForm();
262
+ cb?.();
263
+ });
264
+ },
265
+ [translateY, backdropOpacity, resetForm]
266
+ );
267
+ useEffect(() => {
268
+ if (visible) animateIn();
269
+ }, [visible, animateIn]);
270
+ const handleClose = useCallback(() => {
271
+ animateOut(onClose);
272
+ }, [animateOut, onClose]);
273
+ const panResponder = useRef(
274
+ PanResponder.create({
275
+ onStartShouldSetPanResponder: () => true,
276
+ onMoveShouldSetPanResponder: (_, g) => g.dy > 4,
277
+ onPanResponderMove: (_, g) => {
278
+ if (g.dy > 0) translateY.setValue(g.dy);
279
+ },
280
+ onPanResponderRelease: (_, g) => {
281
+ if (g.dy > DISMISS_THRESHOLD) {
282
+ handleClose();
283
+ } else {
284
+ Animated.spring(translateY, {
285
+ toValue: 0,
286
+ useNativeDriver: true,
287
+ damping: 20,
288
+ stiffness: 200
289
+ }).start();
290
+ }
291
+ }
292
+ })
293
+ ).current;
294
+ const handleSubmit = async () => {
295
+ if (!category || !description.trim() || !mushi) return;
296
+ setPhase("sending");
297
+ try {
298
+ await mushi.submitReport({ category, description: description.trim() });
299
+ setPhase("sent");
300
+ setTimeout(handleClose, 1400);
301
+ } catch {
302
+ setPhase("form");
303
+ }
304
+ };
305
+ const colors = dark ? { bg: "#1c1c1e", text: "#f2f2f7", sub: "#8e8e93", card: "#2c2c2e", accent: "#0a84ff", border: "#38383a", backdrop: "rgba(0,0,0,0.6)" } : { bg: "#ffffff", text: "#1c1c1e", sub: "#8e8e93", card: "#f2f2f7", accent: "#007aff", border: "#e5e5ea", backdrop: "rgba(0,0,0,0.35)" };
306
+ const canSubmit = !!category && description.trim().length > 0 && phase === "form";
307
+ return /* @__PURE__ */ jsx(Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ jsxs(
308
+ KeyboardAvoidingView,
309
+ {
310
+ style: s.flex,
311
+ behavior: Platform2.OS === "ios" ? "padding" : void 0,
312
+ children: [
313
+ /* @__PURE__ */ jsx(
314
+ Animated.View,
315
+ {
316
+ style: [s.backdrop, { backgroundColor: colors.backdrop, opacity: backdropOpacity }],
317
+ children: /* @__PURE__ */ jsx(TouchableOpacity, { style: s.flex, activeOpacity: 1, onPress: handleClose })
318
+ }
319
+ ),
320
+ /* @__PURE__ */ jsxs(
321
+ Animated.View,
322
+ {
323
+ style: [
324
+ s.sheet,
325
+ { backgroundColor: colors.bg, transform: [{ translateY }] }
326
+ ],
327
+ children: [
328
+ /* @__PURE__ */ jsx(View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ jsx(View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
329
+ phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
330
+ /* @__PURE__ */ jsx(Text, { style: [s.sentEmoji], children: "\u2705" }),
331
+ /* @__PURE__ */ jsx(Text, { style: [s.sentText, { color: colors.text }], children: "Report sent!" })
332
+ ] }) : /* @__PURE__ */ jsxs(View, { style: s.body, children: [
333
+ /* @__PURE__ */ jsx(Text, { style: [s.title, { color: colors.text }], children: "Report an issue" }),
334
+ /* @__PURE__ */ jsx(View, { style: s.catRow, children: CATEGORIES.map((c) => {
335
+ const active = category === c.key;
336
+ return /* @__PURE__ */ jsxs(
337
+ TouchableOpacity,
338
+ {
339
+ onPress: () => setCategory(c.key),
340
+ activeOpacity: 0.7,
341
+ style: [
342
+ s.catBtn,
343
+ {
344
+ backgroundColor: active ? colors.accent : colors.card,
345
+ borderColor: active ? colors.accent : colors.border
346
+ }
347
+ ],
348
+ children: [
349
+ /* @__PURE__ */ jsx(Text, { style: s.catEmoji, children: c.emoji }),
350
+ /* @__PURE__ */ jsx(
351
+ Text,
352
+ {
353
+ style: [
354
+ s.catLabel,
355
+ { color: active ? "#fff" : colors.text }
356
+ ],
357
+ children: c.label
358
+ }
359
+ )
360
+ ]
361
+ },
362
+ c.key
363
+ );
364
+ }) }),
365
+ /* @__PURE__ */ jsx(
366
+ TextInput,
367
+ {
368
+ style: [
369
+ s.input,
370
+ {
371
+ backgroundColor: colors.card,
372
+ color: colors.text,
373
+ borderColor: colors.border
374
+ }
375
+ ],
376
+ placeholder: "What happened?",
377
+ placeholderTextColor: colors.sub,
378
+ multiline: true,
379
+ textAlignVertical: "top",
380
+ value: description,
381
+ onChangeText: setDescription,
382
+ editable: phase === "form"
383
+ }
384
+ ),
385
+ /* @__PURE__ */ jsx(
386
+ TouchableOpacity,
387
+ {
388
+ onPress: handleSubmit,
389
+ disabled: !canSubmit,
390
+ activeOpacity: 0.8,
391
+ style: [
392
+ s.submitBtn,
393
+ { backgroundColor: canSubmit ? colors.accent : colors.border }
394
+ ],
395
+ children: /* @__PURE__ */ jsx(Text, { style: s.submitText, children: phase === "sending" ? "Sending\u2026" : "Submit" })
396
+ }
397
+ )
398
+ ] })
399
+ ]
400
+ }
401
+ )
402
+ ]
403
+ }
404
+ ) });
405
+ };
406
+ var s = StyleSheet.create({
407
+ flex: { flex: 1 },
408
+ backdrop: {
409
+ ...StyleSheet.absoluteFillObject
410
+ },
411
+ sheet: {
412
+ position: "absolute",
413
+ bottom: 0,
414
+ left: 0,
415
+ right: 0,
416
+ minHeight: SHEET_HEIGHT,
417
+ borderTopLeftRadius: 20,
418
+ borderTopRightRadius: 20,
419
+ paddingBottom: Platform2.OS === "ios" ? 34 : 16,
420
+ shadowColor: "#000",
421
+ shadowOffset: { width: 0, height: -3 },
422
+ shadowOpacity: 0.12,
423
+ shadowRadius: 8,
424
+ elevation: 24
425
+ },
426
+ handleArea: {
427
+ alignItems: "center",
428
+ paddingVertical: 10
429
+ },
430
+ handle: {
431
+ width: 40,
432
+ height: 5,
433
+ borderRadius: 3,
434
+ opacity: 0.5
435
+ },
436
+ body: {
437
+ paddingHorizontal: 20,
438
+ paddingTop: 4
439
+ },
440
+ title: {
441
+ fontSize: 20,
442
+ fontWeight: "700",
443
+ marginBottom: 16
444
+ },
445
+ catRow: {
446
+ flexDirection: "row",
447
+ justifyContent: "space-between",
448
+ marginBottom: 16
449
+ },
450
+ catBtn: {
451
+ alignItems: "center",
452
+ paddingVertical: 10,
453
+ paddingHorizontal: 6,
454
+ borderRadius: 12,
455
+ borderWidth: 1,
456
+ flex: 1,
457
+ marginHorizontal: 3
458
+ },
459
+ catEmoji: {
460
+ fontSize: 22,
461
+ marginBottom: 4
462
+ },
463
+ catLabel: {
464
+ fontSize: 11,
465
+ fontWeight: "600"
466
+ },
467
+ input: {
468
+ borderWidth: 1,
469
+ borderRadius: 12,
470
+ padding: 14,
471
+ fontSize: 15,
472
+ minHeight: 100,
473
+ marginBottom: 16
474
+ },
475
+ submitBtn: {
476
+ borderRadius: 12,
477
+ paddingVertical: 14,
478
+ alignItems: "center"
479
+ },
480
+ submitText: {
481
+ color: "#fff",
482
+ fontSize: 16,
483
+ fontWeight: "700"
484
+ },
485
+ sentWrap: {
486
+ alignItems: "center",
487
+ justifyContent: "center",
488
+ paddingVertical: 60
489
+ },
490
+ sentEmoji: {
491
+ fontSize: 48,
492
+ marginBottom: 12
493
+ },
494
+ sentText: {
495
+ fontSize: 20,
496
+ fontWeight: "600"
497
+ }
498
+ });
499
+
500
+ // src/components/MushiFloatingButton.tsx
501
+ import { useRef as useRef2, useCallback as useCallback2 } from "react";
502
+ import {
503
+ Animated as Animated2,
504
+ TouchableWithoutFeedback,
505
+ StyleSheet as StyleSheet2,
506
+ Platform as Platform3,
507
+ useColorScheme as useColorScheme2
508
+ } from "react-native";
509
+ import { jsx as jsx2 } from "react/jsx-runtime";
510
+ var MushiFloatingButton = ({
511
+ onPress,
512
+ position = "bottom-right"
513
+ }) => {
514
+ const scheme = useColorScheme2();
515
+ const dark = scheme === "dark";
516
+ const scale = useRef2(new Animated2.Value(1)).current;
517
+ const handlePressIn = useCallback2(() => {
518
+ Animated2.spring(scale, {
519
+ toValue: 0.85,
520
+ useNativeDriver: true,
521
+ damping: 15,
522
+ stiffness: 300
523
+ }).start();
524
+ }, [scale]);
525
+ const handlePressOut = useCallback2(() => {
526
+ Animated2.spring(scale, {
527
+ toValue: 1,
528
+ useNativeDriver: true,
529
+ damping: 10,
530
+ stiffness: 250
531
+ }).start();
532
+ }, [scale]);
533
+ const positionStyle = position === "bottom-left" ? { left: 20 } : { right: 20 };
534
+ return /* @__PURE__ */ jsx2(
535
+ TouchableWithoutFeedback,
536
+ {
537
+ onPress,
538
+ onPressIn: handlePressIn,
539
+ onPressOut: handlePressOut,
540
+ children: /* @__PURE__ */ jsx2(
541
+ Animated2.View,
542
+ {
543
+ style: [
544
+ s2.fab,
545
+ positionStyle,
546
+ {
547
+ backgroundColor: dark ? "#0a84ff" : "#007aff",
548
+ transform: [{ scale }],
549
+ shadowColor: dark ? "#0a84ff" : "#000"
550
+ }
551
+ ],
552
+ children: /* @__PURE__ */ jsx2(Animated2.Text, { style: s2.emoji, children: "\u{1F41B}" })
553
+ }
554
+ )
555
+ }
556
+ );
557
+ };
558
+ var s2 = StyleSheet2.create({
559
+ fab: {
560
+ position: "absolute",
561
+ bottom: Platform3.OS === "ios" ? 50 : 28,
562
+ width: 56,
563
+ height: 56,
564
+ borderRadius: 28,
565
+ alignItems: "center",
566
+ justifyContent: "center",
567
+ zIndex: 9999,
568
+ elevation: 8,
569
+ shadowOffset: { width: 0, height: 4 },
570
+ shadowOpacity: 0.3,
571
+ shadowRadius: 6
572
+ },
573
+ emoji: {
574
+ fontSize: 26
575
+ }
576
+ });
577
+
177
578
  // src/provider.tsx
178
- import { jsx } from "react/jsx-runtime";
579
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
179
580
  var MushiContext = createContext(null);
180
581
  function MushiProvider({ children, ...config }) {
181
- const consoleRef = useRef(null);
182
- const networkRef = useRef(null);
183
- const queueRef = useRef(null);
184
- const apiEndpoint = config.endpoint ?? "https://api.mushimushi.dev";
185
- useEffect(() => {
582
+ const consoleRef = useRef3(null);
583
+ const networkRef = useRef3(null);
584
+ const queueRef = useRef3(null);
585
+ const apiClientRef = useRef3(null);
586
+ const [sheetVisible, setSheetVisible] = useState2(false);
587
+ const apiEndpoint = config.endpoint ?? DEFAULT_API_ENDPOINT;
588
+ useEffect2(() => {
186
589
  if (config.capture?.console !== false) {
187
590
  consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
188
591
  }
189
592
  if (config.capture?.network !== false) {
190
593
  networkRef.current = setupNetworkCapture(config.capture?.maxNetworkEntries, apiEndpoint);
191
594
  }
595
+ apiClientRef.current = createApiClient({
596
+ projectId: config.projectId,
597
+ apiKey: config.apiKey,
598
+ apiEndpoint
599
+ });
192
600
  queueRef.current = new AsyncStorageQueue({
193
601
  maxSize: config.storage?.maxQueueSize,
194
602
  apiEndpoint,
195
603
  apiKey: config.apiKey
196
604
  });
605
+ queueRef.current.flush().catch(() => {
606
+ });
197
607
  return () => {
198
608
  consoleRef.current?.restore();
199
609
  networkRef.current?.restore();
200
610
  };
201
611
  }, []);
202
- const instance = {
203
- open: () => {
204
- },
205
- close: () => {
206
- },
207
- submitReport: async (data) => {
612
+ const open = useCallback3(() => setSheetVisible(true), []);
613
+ const close = useCallback3(() => setSheetVisible(false), []);
614
+ const submitReport = useCallback3(
615
+ async (data) => {
616
+ const deviceInfo = getDeviceInfo();
208
617
  const report = {
618
+ id: `rn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
209
619
  projectId: config.projectId,
210
- ...data,
211
- environment: getDeviceInfo(),
212
- console_logs: consoleRef.current?.getEntries() ?? [],
213
- network_logs: networkRef.current?.getEntries() ?? []
620
+ category: data.category,
621
+ description: data.description,
622
+ environment: {
623
+ userAgent: deviceInfo.systemName ?? "ReactNative",
624
+ platform: deviceInfo.platform ?? "mobile",
625
+ language: deviceInfo.locale ?? "en",
626
+ viewport: { width: deviceInfo.screenWidth ?? 0, height: deviceInfo.screenHeight ?? 0 },
627
+ url: "",
628
+ referrer: "",
629
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
630
+ timezone: deviceInfo.timezone ?? "UTC"
631
+ },
632
+ consoleLogs: consoleRef.current?.getEntries() ?? [],
633
+ networkLogs: networkRef.current?.getEntries().map((entry) => ({
634
+ ...entry,
635
+ status: entry.status ?? 0
636
+ })) ?? [],
637
+ reporterToken: `rn-${config.projectId}-anon`,
638
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
214
639
  };
215
- try {
216
- const res = await fetch(`${apiEndpoint}/v1/reports`, {
217
- method: "POST",
218
- headers: { "Content-Type": "application/json", "X-Mushi-Api-Key": config.apiKey },
219
- body: JSON.stringify(report)
220
- });
221
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
222
- } catch {
640
+ const client = apiClientRef.current;
641
+ if (!client) return;
642
+ const result = await client.submitReport(report);
643
+ if (!result.ok) {
223
644
  await queueRef.current?.enqueue(report);
224
645
  }
225
646
  },
226
- getDeviceInfo,
227
- getConsoleEntries: () => consoleRef.current?.getEntries() ?? [],
228
- getNetworkEntries: () => networkRef.current?.getEntries() ?? []
229
- };
230
- return /* @__PURE__ */ jsx(MushiContext.Provider, { value: instance, children });
647
+ [config.projectId, config.apiKey, apiEndpoint]
648
+ );
649
+ const instance = useMemo(
650
+ () => ({
651
+ open,
652
+ close,
653
+ submitReport,
654
+ getDeviceInfo,
655
+ getConsoleEntries: () => consoleRef.current?.getEntries() ?? [],
656
+ getNetworkEntries: () => networkRef.current?.getEntries() ?? []
657
+ }),
658
+ [open, close, submitReport]
659
+ );
660
+ const trigger = config.widget?.trigger ?? "button";
661
+ const showFab = trigger === "button" || trigger === "both";
662
+ return /* @__PURE__ */ jsxs2(MushiContext.Provider, { value: instance, children: [
663
+ children,
664
+ showFab && /* @__PURE__ */ jsx3(
665
+ MushiFloatingButton,
666
+ {
667
+ onPress: open,
668
+ position: config.widget?.buttonPosition
669
+ }
670
+ ),
671
+ /* @__PURE__ */ jsx3(MushiBottomSheet, { visible: sheetVisible, onClose: close })
672
+ ] });
231
673
  }
232
674
  function useMushiContext() {
233
675
  return useContext(MushiContext);
@@ -239,10 +681,10 @@ function useMushi() {
239
681
  }
240
682
 
241
683
  // src/hooks/useMushiReport.ts
242
- import { useCallback } from "react";
684
+ import { useCallback as useCallback4 } from "react";
243
685
  function useMushiReport() {
244
686
  const mushi = useMushiContext();
245
- const submitReport = useCallback(
687
+ const submitReport = useCallback4(
246
688
  async (data) => {
247
689
  if (!mushi) throw new Error("MushiProvider not found");
248
690
  await mushi.submitReport(data);
@@ -253,20 +695,20 @@ function useMushiReport() {
253
695
  }
254
696
 
255
697
  // src/hooks/useMushiWidget.ts
256
- import { useCallback as useCallback2 } from "react";
698
+ import { useCallback as useCallback5 } from "react";
257
699
  function useMushiWidget() {
258
700
  const mushi = useMushiContext();
259
701
  return {
260
- open: useCallback2(() => mushi?.open(), [mushi]),
261
- close: useCallback2(() => mushi?.close(), [mushi])
702
+ open: useCallback5(() => mushi?.open(), [mushi]),
703
+ close: useCallback5(() => mushi?.close(), [mushi])
262
704
  };
263
705
  }
264
706
 
265
707
  // src/capture/navigation-capture.ts
266
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
708
+ import { useEffect as useEffect3, useRef as useRef4 } from "react";
267
709
  function useNavigationCapture(navigationRef) {
268
- const historyRef = useRef2([]);
269
- useEffect2(() => {
710
+ const historyRef = useRef4([]);
711
+ useEffect3(() => {
270
712
  if (!navigationRef?.addListener || !navigationRef?.getCurrentRoute) return;
271
713
  const unsubscribe = navigationRef.addListener("state", () => {
272
714
  const route = navigationRef.getCurrentRoute?.();
@@ -281,6 +723,8 @@ function useNavigationCapture(navigationRef) {
281
723
  }
282
724
  export {
283
725
  AsyncStorageQueue,
726
+ MushiBottomSheet,
727
+ MushiFloatingButton,
284
728
  MushiProvider,
285
729
  getDeviceInfo,
286
730
  setupConsoleCapture,