@upstep/react-native 0.5.1 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upstep/react-native",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Upstep feedback SDK for React Native",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -15,11 +15,8 @@
15
15
  "@upstep/types": "0.5.0"
16
16
  },
17
17
  "peerDependencies": {
18
- "@gorhom/bottom-sheet": ">=4",
19
18
  "react": ">=18",
20
- "react-native": ">=0.73",
21
- "react-native-gesture-handler": ">=2",
22
- "react-native-reanimated": ">=3"
19
+ "react-native": ">=0.73"
23
20
  },
24
21
  "devDependencies": {
25
22
  "@types/react": "^19.0.7",
@@ -4,7 +4,6 @@ import React, {
4
4
  useEffect,
5
5
  useState,
6
6
  useCallback,
7
- useRef,
8
7
  type ReactNode,
9
8
  } from "react";
10
9
  import type {
@@ -15,7 +14,6 @@ import type {
15
14
  VoteValue,
16
15
  } from "@upstep/types";
17
16
  import { UpstepApiClient } from "./api";
18
- import type BottomSheet from "@gorhom/bottom-sheet";
19
17
 
20
18
  const DEFAULT_ACCENT = "#D97757";
21
19
 
@@ -27,11 +25,10 @@ interface UpstepContextValue {
27
25
  submit: (title: string, content: string, type?: FeedbackType) => Promise<void>;
28
26
  vote: (feedbackId: string, value: VoteValue) => Promise<void>;
29
27
  getItem: (feedbackId: string) => Promise<FeedbackWithComments>;
30
- /** Open the feedback sheet — call from your own UI (settings, menu, etc.). */
28
+ isOpen: boolean;
31
29
  openSheet: () => void;
32
- /** Set / update the end-user id at runtime (e.g. after the user logs in). */
30
+ closeSheet: () => void;
33
31
  identify: (userId: string | undefined) => void;
34
- sheetRef: React.RefObject<BottomSheet | null>;
35
32
  accentColor: string;
36
33
  theme: "light" | "dark" | "auto";
37
34
  }
@@ -45,7 +42,7 @@ export function FeedbackProvider({
45
42
  const [client] = useState(() => new UpstepApiClient(config));
46
43
  const [feedItems, setFeedItems] = useState<Feedback[]>([]);
47
44
  const [feedLoading, setFeedLoading] = useState(true);
48
- const sheetRef = useRef<BottomSheet>(null);
45
+ const [isOpen, setIsOpen] = useState(false);
49
46
 
50
47
  useEffect(() => {
51
48
  client.setUserId(config.userId);
@@ -87,7 +84,11 @@ export function FeedbackProvider({
87
84
  );
88
85
 
89
86
  const openSheet = useCallback(() => {
90
- sheetRef.current?.expand();
87
+ setIsOpen(true);
88
+ }, []);
89
+
90
+ const closeSheet = useCallback(() => {
91
+ setIsOpen(false);
91
92
  }, []);
92
93
 
93
94
  const identify = useCallback(
@@ -99,7 +100,7 @@ export function FeedbackProvider({
99
100
  <UpstepContext.Provider
100
101
  value={{
101
102
  client, feedItems, feedLoading, loadFeed, submit, vote, getItem,
102
- openSheet, identify, sheetRef,
103
+ isOpen, openSheet, closeSheet, identify,
103
104
  accentColor: config.accentColor ?? DEFAULT_ACCENT,
104
105
  theme: config.theme ?? "auto",
105
106
  }}
@@ -1,20 +1,19 @@
1
- import React, { useState, useMemo, useCallback, useEffect } from "react";
1
+ import React, { useState, useCallback, useEffect } from "react";
2
2
  import {
3
+ Modal,
3
4
  View,
4
5
  Text,
5
6
  TextInput,
6
7
  TouchableOpacity,
8
+ Pressable,
9
+ ScrollView,
10
+ KeyboardAvoidingView,
7
11
  StyleSheet,
8
12
  ActivityIndicator,
9
13
  useColorScheme,
10
14
  Platform,
15
+ SafeAreaView,
11
16
  } from "react-native";
12
- import BottomSheet, {
13
- BottomSheetView,
14
- BottomSheetScrollView,
15
- BottomSheetBackdrop,
16
- } from "@gorhom/bottom-sheet";
17
- import type { BottomSheetBackdropProps } from "@gorhom/bottom-sheet";
18
17
  import type { Comment, Feedback, FeedbackType, FeedbackWithComments } from "@upstep/types";
19
18
  import { useUpstep } from "./FeedbackContext";
20
19
 
@@ -58,116 +57,119 @@ type Nav =
58
57
  | { screen: "create" };
59
58
 
60
59
  // ─── FeedbackSheet ────────────────────────────────────────────────────────────
61
- //
62
- // IMPORTANT: BottomSheet requires stable direct children — never conditionally
63
- // render <BottomSheetView> or <BottomSheetScrollView> as they set up gesture
64
- // handlers on mount. We keep ONE of each always mounted; only their contents
65
- // change based on `nav`. This also fixes TouchableOpacity not responding.
66
60
 
67
61
  export function FeedbackSheet() {
68
- const { sheetRef, accentColor, theme, loadFeed } = useUpstep();
62
+ const { isOpen, closeSheet, accentColor, theme, loadFeed } = useUpstep();
69
63
  const scheme = useColorScheme();
70
64
  const isDark = theme === "dark" || (theme === "auto" && scheme === "dark");
71
65
  const p = isDark ? DARK : LIGHT;
72
66
 
73
67
  const [nav, setNav] = useState<Nav>({ screen: "list" });
74
- const snapPoints = useMemo(() => ["90%"], []);
75
-
76
- // Dim backdrop — tap to close
77
- const renderBackdrop = useCallback(
78
- (props: BottomSheetBackdropProps) => (
79
- <BottomSheetBackdrop
80
- {...props}
81
- disappearsOnIndex={-1}
82
- appearsOnIndex={0}
83
- pressBehavior="close"
84
- />
85
- ),
86
- []
87
- );
68
+
69
+ // Refresh list every time the modal opens
70
+ useEffect(() => {
71
+ if (isOpen) loadFeed();
72
+ }, [isOpen, loadFeed]);
73
+
74
+ function handleClose() {
75
+ closeSheet();
76
+ // Reset to list when closed so next open starts fresh
77
+ setTimeout(() => setNav({ screen: "list" }), 300);
78
+ }
88
79
 
89
80
  return (
90
- <BottomSheet
91
- ref={sheetRef}
92
- index={-1}
93
- snapPoints={snapPoints}
94
- enablePanDownToClose
95
- backdropComponent={renderBackdrop}
96
- backgroundStyle={{ backgroundColor: p.bg, borderRadius: 24 }}
97
- handleIndicatorStyle={{ backgroundColor: p.handle, width: 40, height: 4 }}
98
- onChange={(index) => { if (index >= 0) loadFeed(); }}
99
- onClose={() => setNav({ screen: "list" })}
81
+ <Modal
82
+ visible={isOpen}
83
+ animationType="slide"
84
+ transparent
85
+ statusBarTranslucent
86
+ onRequestClose={handleClose}
100
87
  >
101
- {/*
102
- Wrap everything in ONE BottomSheetView so the library can measure the
103
- sheet correctly. A plain View header stays fixed; BottomSheetScrollView
104
- fills the remaining space below it — no overlap.
105
- */}
106
- <BottomSheetView style={styles.sheetContainer}>
107
- {/* ── Fixed header ─────────────────────────────── */}
108
- <View style={[styles.header, { borderBottomColor: p.border }]}>
109
- {nav.screen === "list" ? (
110
- <>
111
- <Text style={[styles.headerTitle, { color: p.text }]}>Feedback</Text>
112
- <TouchableOpacity
113
- onPress={() => setNav({ screen: "create" })}
114
- activeOpacity={0.8}
115
- style={[styles.newBtn, { backgroundColor: accentColor }]}
116
- >
117
- <Text style={styles.newBtnText}>+ New</Text>
118
- </TouchableOpacity>
119
- </>
120
- ) : nav.screen === "create" ? (
121
- <>
88
+ <KeyboardAvoidingView
89
+ style={styles.overlay}
90
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
91
+ >
92
+ {/* Tap backdrop to dismiss */}
93
+ <Pressable style={styles.backdrop} onPress={handleClose} />
94
+
95
+ {/* Sheet */}
96
+ <View style={[styles.sheet, { backgroundColor: p.bg }]}>
97
+ {/* Drag handle */}
98
+ <View style={styles.handleWrap}>
99
+ <View style={[styles.handle, { backgroundColor: p.handle }]} />
100
+ </View>
101
+
102
+ {/* Fixed header */}
103
+ <View style={[styles.header, { borderBottomColor: p.border }]}>
104
+ {nav.screen === "list" ? (
105
+ <>
106
+ <Text style={[styles.headerTitle, { color: p.text }]}>Feedback</Text>
107
+ <TouchableOpacity
108
+ onPress={() => setNav({ screen: "create" })}
109
+ activeOpacity={0.8}
110
+ style={[styles.newBtn, { backgroundColor: accentColor }]}
111
+ >
112
+ <Text style={styles.newBtnText}>+ New</Text>
113
+ </TouchableOpacity>
114
+ </>
115
+ ) : nav.screen === "create" ? (
116
+ <>
117
+ <TouchableOpacity
118
+ onPress={() => setNav({ screen: "list" })}
119
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
120
+ >
121
+ <Text style={[styles.navBtn, { color: accentColor }]}>Cancel</Text>
122
+ </TouchableOpacity>
123
+ <Text style={[styles.headerTitle, { color: p.text }]}>New feedback</Text>
124
+ <View style={styles.navBtnSpacer} />
125
+ </>
126
+ ) : (
122
127
  <TouchableOpacity
123
128
  onPress={() => setNav({ screen: "list" })}
124
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 16 }}
129
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
125
130
  >
126
- <Text style={[styles.navBtn, { color: accentColor }]}>Cancel</Text>
131
+ <Text style={[styles.navBtn, { color: accentColor }]}>← Back</Text>
127
132
  </TouchableOpacity>
128
- <Text style={[styles.headerTitle, { color: p.text }]}>New feedback</Text>
129
- <View style={styles.navBtnPlaceholder} />
130
- </>
131
- ) : (
132
- <TouchableOpacity
133
- onPress={() => setNav({ screen: "list" })}
134
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 16 }}
135
- >
136
- <Text style={[styles.navBtn, { color: accentColor }]}>← Back</Text>
137
- </TouchableOpacity>
138
- )}
133
+ )}
134
+ </View>
135
+
136
+ {/* Scrollable body */}
137
+ <ScrollView
138
+ style={styles.scroll}
139
+ contentContainerStyle={styles.scrollContent}
140
+ keyboardShouldPersistTaps="handled"
141
+ showsVerticalScrollIndicator={false}
142
+ >
143
+ {nav.screen === "list" && (
144
+ <FeedListBody
145
+ p={p}
146
+ accent={accentColor}
147
+ onSelect={(id) => setNav({ screen: "detail", feedbackId: id })}
148
+ />
149
+ )}
150
+ {nav.screen === "detail" && "feedbackId" in nav && (
151
+ <FeedDetailBody
152
+ p={p}
153
+ accent={accentColor}
154
+ feedbackId={(nav as { screen: "detail"; feedbackId: string }).feedbackId}
155
+ />
156
+ )}
157
+ {nav.screen === "create" && (
158
+ <FeedCreateBody
159
+ p={p}
160
+ accent={accentColor}
161
+ onDone={() => {
162
+ setNav({ screen: "list" });
163
+ }}
164
+ />
165
+ )}
166
+ </ScrollView>
167
+
168
+ {/* Safe area spacer */}
169
+ <SafeAreaView />
139
170
  </View>
140
-
141
- {/* ── Scrollable body ───────────────────────────── */}
142
- <BottomSheetScrollView
143
- style={styles.scrollView}
144
- contentContainerStyle={styles.scrollBody}
145
- keyboardShouldPersistTaps="handled"
146
- >
147
- {nav.screen === "list" && (
148
- <FeedListBody
149
- p={p}
150
- accent={accentColor}
151
- onSelect={(id) => setNav({ screen: "detail", feedbackId: id })}
152
- />
153
- )}
154
- {nav.screen === "detail" && "feedbackId" in nav && (
155
- <FeedDetailBody
156
- p={p}
157
- accent={accentColor}
158
- feedbackId={(nav as { screen: "detail"; feedbackId: string }).feedbackId}
159
- />
160
- )}
161
- {nav.screen === "create" && (
162
- <FeedCreateBody
163
- p={p}
164
- accent={accentColor}
165
- onDone={() => setNav({ screen: "list" })}
166
- />
167
- )}
168
- </BottomSheetScrollView>
169
- </BottomSheetView>
170
- </BottomSheet>
171
+ </KeyboardAvoidingView>
172
+ </Modal>
171
173
  );
172
174
  }
173
175
 
@@ -176,8 +178,7 @@ export function FeedbackSheet() {
176
178
  function FeedListBody({
177
179
  p, accent, onSelect,
178
180
  }: {
179
- p: Palette; accent: string;
180
- onSelect: (id: string) => void;
181
+ p: Palette; accent: string; onSelect: (id: string) => void;
181
182
  }) {
182
183
  const { feedItems, feedLoading, vote } = useUpstep();
183
184
 
@@ -200,7 +201,7 @@ function FeedListBody({
200
201
  }
201
202
 
202
203
  return (
203
- <View style={styles.listPad}>
204
+ <>
204
205
  {feedItems.map((f) => (
205
206
  <FeedCard
206
207
  key={f.id}
@@ -211,7 +212,7 @@ function FeedListBody({
211
212
  onVote={() => vote(f.id, "UP")}
212
213
  />
213
214
  ))}
214
- </View>
215
+ </>
215
216
  );
216
217
  }
217
218
 
@@ -219,8 +220,7 @@ function FeedCard({
219
220
  item, p, accent, onPress, onVote,
220
221
  }: {
221
222
  item: Feedback; p: Palette; accent: string;
222
- onPress: () => void;
223
- onVote: () => void;
223
+ onPress: () => void; onVote: () => void;
224
224
  }) {
225
225
  const voted = item.userVote === "UP";
226
226
  const displayTitle = item.title ?? item.content;
@@ -259,9 +259,7 @@ function FeedCard({
259
259
  <View style={styles.cardMeta}>
260
260
  <TypeBadge type={item.type} p={p} />
261
261
  <StatusDot status={item.status} />
262
- <Text style={[styles.cardDate, { color: p.textFaint }]}>
263
- {fmtDate(item.createdAt)}
264
- </Text>
262
+ <Text style={[styles.cardDate, { color: p.textFaint }]}>{fmtDate(item.createdAt)}</Text>
265
263
  </View>
266
264
  </View>
267
265
 
@@ -350,9 +348,7 @@ function FeedDetailBody({
350
348
 
351
349
  {item.comments.length > 0 && (
352
350
  <View style={[styles.commentsSection, { borderTopColor: p.border }]}>
353
- <Text style={[styles.commentsLabel, { color: p.textFaint }]}>
354
- DEVELOPER RESPONSE
355
- </Text>
351
+ <Text style={[styles.commentsLabel, { color: p.textFaint }]}>DEVELOPER RESPONSE</Text>
356
352
  {item.comments.map((c) => (
357
353
  <CommentBubble key={c.id} comment={c} p={p} accent={accent} />
358
354
  ))}
@@ -371,9 +367,7 @@ function CommentBubble({ comment, p, accent }: { comment: Comment; p: Palette; a
371
367
  {comment.authorName ?? "Team"}
372
368
  </Text>
373
369
  </View>
374
- <Text style={[styles.commentDate, { color: p.textFaint }]}>
375
- {fmtDate(comment.createdAt)}
376
- </Text>
370
+ <Text style={[styles.commentDate, { color: p.textFaint }]}>{fmtDate(comment.createdAt)}</Text>
377
371
  </View>
378
372
  <Text style={[styles.commentContent, { color: p.textSoft }]}>{comment.content}</Text>
379
373
  </View>
@@ -409,7 +403,7 @@ function FeedCreateBody({
409
403
  try {
410
404
  await submit(title.trim(), description.trim() || title.trim(), type);
411
405
  setSuccess(true);
412
- setTimeout(onDone, 1400);
406
+ setTimeout(onDone, 1200);
413
407
  } catch {
414
408
  setError("Something went wrong. Please try again.");
415
409
  } finally {
@@ -419,7 +413,7 @@ function FeedCreateBody({
419
413
 
420
414
  if (success) {
421
415
  return (
422
- <View style={[styles.centered, { paddingTop: 60 }]}>
416
+ <View style={[styles.centered, { paddingTop: 40 }]}>
423
417
  <Text style={[styles.successTitle, { color: p.text }]}>Submitted</Text>
424
418
  <Text style={[styles.successSub, { color: p.textFaint }]}>
425
419
  Thanks — we received your feedback.
@@ -519,11 +513,8 @@ function TypeBadge({ type, p }: { type: FeedbackType; p: Palette }) {
519
513
  }
520
514
 
521
515
  const STATUS_COLOR: Record<string, string> = {
522
- OPEN: "#b45309",
523
- IN_PROGRESS: "#1d4ed8",
524
- DONE: "#15803d",
525
- PENDING: "#ea580c",
526
- CLOSED: "#6b7280",
516
+ OPEN: "#b45309", IN_PROGRESS: "#1d4ed8", DONE: "#15803d",
517
+ PENDING: "#ea580c", CLOSED: "#6b7280",
527
518
  };
528
519
 
529
520
  function StatusDot({ status }: { status: string }) {
@@ -534,9 +525,7 @@ function StatusBadge({ status, p }: { status: string; p: Palette }) {
534
525
  return (
535
526
  <View style={[styles.statusBadge, { backgroundColor: p.bgSoft, borderColor: p.border }]}>
536
527
  <StatusDot status={status} />
537
- <Text style={[styles.statusBadgeText, { color: p.textSoft }]}>
538
- {status.replace("_", " ")}
539
- </Text>
528
+ <Text style={[styles.statusBadgeText, { color: p.textSoft }]}>{status.replace("_", " ")}</Text>
540
529
  </View>
541
530
  );
542
531
  }
@@ -548,9 +537,35 @@ function fmtDate(iso: string) {
548
537
  // ─── Styles ───────────────────────────────────────────────────────────────────
549
538
 
550
539
  const styles = StyleSheet.create({
551
- // Sheet container — fills the sheet, stacks header above scroll
552
- sheetContainer: { flex: 1 },
540
+ // Modal overlay
541
+ overlay: {
542
+ flex: 1,
543
+ justifyContent: "flex-end",
544
+ },
545
+ backdrop: {
546
+ ...StyleSheet.absoluteFill,
547
+ backgroundColor: "rgba(0,0,0,0.45)",
548
+ },
549
+
550
+ // Sheet container
551
+ sheet: {
552
+ maxHeight: "92%",
553
+ borderTopLeftRadius: 24,
554
+ borderTopRightRadius: 24,
555
+ overflow: "hidden",
556
+ },
557
+ handleWrap: {
558
+ alignItems: "center",
559
+ paddingTop: 10,
560
+ paddingBottom: 4,
561
+ },
562
+ handle: {
563
+ width: 40,
564
+ height: 4,
565
+ borderRadius: 2,
566
+ },
553
567
 
568
+ // Header
554
569
  header: {
555
570
  flexDirection: "row",
556
571
  alignItems: "center",
@@ -561,15 +576,15 @@ const styles = StyleSheet.create({
561
576
  },
562
577
  headerTitle: { fontSize: 16, fontWeight: "700", letterSpacing: -0.3 },
563
578
  navBtn: { fontSize: 14, fontWeight: "600" },
564
- navBtnPlaceholder: { width: 60 },
579
+ navBtnSpacer: { width: 60 },
565
580
  newBtn: { borderRadius: 20, paddingHorizontal: 14, paddingVertical: 7 },
566
581
  newBtnText: { color: "#fff", fontSize: 13, fontWeight: "700" },
567
582
 
568
- scrollView: { flex: 1 },
569
- scrollBody: { flexGrow: 1 },
583
+ // Scroll
584
+ scroll: { flexGrow: 0 },
585
+ scrollContent: { flexGrow: 1, padding: 16, paddingBottom: 32 },
570
586
 
571
587
  // List
572
- listPad: { padding: 16, paddingBottom: 40 },
573
588
  centered: { alignItems: "center", justifyContent: "center", paddingVertical: 60 },
574
589
  emptyText: { fontSize: 14 },
575
590
  card: {
@@ -599,7 +614,7 @@ const styles = StyleSheet.create({
599
614
  chevron: { fontSize: 22, fontWeight: "300", marginLeft: 4 },
600
615
 
601
616
  // Detail
602
- detailPad: { padding: 20, paddingBottom: 50 },
617
+ detailPad: { paddingTop: 4 },
603
618
  detailTitle: { fontSize: 22, fontWeight: "700", letterSpacing: -0.4, marginBottom: 10, lineHeight: 28 },
604
619
  badgeRow: { flexDirection: "row", gap: 8, marginBottom: 16 },
605
620
  detailContent: { fontSize: 15, lineHeight: 23, marginBottom: 24 },
@@ -615,8 +630,7 @@ const styles = StyleSheet.create({
615
630
  commentsSection: { marginTop: 28, borderTopWidth: StyleSheet.hairlineWidth, paddingTop: 20 },
616
631
  commentsLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 12 },
617
632
  commentBubble: {
618
- borderRadius: 12, borderWidth: StyleSheet.hairlineWidth,
619
- padding: 14, marginBottom: 10,
633
+ borderRadius: 12, borderWidth: StyleSheet.hairlineWidth, padding: 14, marginBottom: 10,
620
634
  },
621
635
  commentMeta: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
622
636
  ownerBadge: { borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3 },
@@ -625,7 +639,7 @@ const styles = StyleSheet.create({
625
639
  commentContent: { fontSize: 14, lineHeight: 21 },
626
640
 
627
641
  // Create
628
- createPad: { padding: 20, paddingBottom: 50 },
642
+ createPad: { paddingTop: 4 },
629
643
  fieldLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 8 },
630
644
  typeRow: { flexDirection: "row", gap: 8, marginBottom: 20, flexWrap: "wrap" },
631
645
  typeBtn: { paddingVertical: 9, paddingHorizontal: 14, borderRadius: 10, borderWidth: 1 },