@upstep/react-native 0.3.0 → 0.4.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.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Upstep feedback SDK for React Native",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -12,7 +12,7 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "@upstep/types": "0.2.0"
15
+ "@upstep/types": "0.4.0"
16
16
  },
17
17
  "peerDependencies": {
18
18
  "@gorhom/bottom-sheet": ">=4",
@@ -8,14 +8,19 @@ interface Props {
8
8
  }
9
9
 
10
10
  export function FeedbackButton({ position = "bottom-right", label = "Feedback" }: Props) {
11
- const { openSheet } = useUpstep();
11
+ const { openSheet, accentColor } = useUpstep();
12
12
 
13
13
  return (
14
14
  <TouchableOpacity
15
15
  onPress={openSheet}
16
- style={[styles.btn, position === "bottom-left" ? styles.left : styles.right]}
16
+ style={[
17
+ styles.btn,
18
+ { backgroundColor: accentColor },
19
+ position === "bottom-left" ? styles.left : styles.right,
20
+ ]}
17
21
  activeOpacity={0.85}
18
22
  >
23
+ <Text style={styles.icon}>💬</Text>
19
24
  <Text style={styles.label}>{label}</Text>
20
25
  </TouchableOpacity>
21
26
  );
@@ -25,18 +30,21 @@ const styles = StyleSheet.create({
25
30
  btn: {
26
31
  position: "absolute",
27
32
  bottom: 24,
28
- backgroundColor: "#6366f1",
33
+ flexDirection: "row",
34
+ alignItems: "center",
35
+ gap: 6,
29
36
  borderRadius: 9999,
30
37
  paddingVertical: 12,
31
- paddingHorizontal: 20,
38
+ paddingHorizontal: 18,
32
39
  shadowColor: "#000",
33
- shadowOffset: { width: 0, height: 4 },
40
+ shadowOffset: { width: 0, height: 5 },
34
41
  shadowOpacity: 0.2,
35
- shadowRadius: 6,
42
+ shadowRadius: 10,
36
43
  elevation: 6,
37
44
  zIndex: 100,
38
45
  },
39
46
  right: { right: 20 },
40
47
  left: { left: 20 },
48
+ icon: { fontSize: 14 },
41
49
  label: { color: "#fff", fontSize: 14, fontWeight: "600" },
42
50
  });
@@ -11,6 +11,8 @@ import type { Feedback, FeedbackType, UpstepConfig, VoteValue } from "@upstep/ty
11
11
  import { UpstepApiClient } from "./api";
12
12
  import type BottomSheet from "@gorhom/bottom-sheet";
13
13
 
14
+ const DEFAULT_ACCENT = "#D97757";
15
+
14
16
  interface UpstepContextValue {
15
17
  client: UpstepApiClient;
16
18
  feedItems: Feedback[];
@@ -22,6 +24,10 @@ interface UpstepContextValue {
22
24
  /** Set / update the end-user id at runtime (e.g. after the user logs in). */
23
25
  identify: (userId: string | undefined) => void;
24
26
  sheetRef: React.RefObject<BottomSheet | null>;
27
+ /** Accent color from provider config (default applied). */
28
+ accentColor: string;
29
+ /** Theme mode from provider config (default "auto"). */
30
+ theme: "light" | "dark" | "auto";
25
31
  }
26
32
 
27
33
  const UpstepContext = createContext<UpstepContextValue | null>(null);
@@ -69,7 +75,13 @@ export function FeedbackProvider({
69
75
  const identify = useCallback((userId: string | undefined) => client.setUserId(userId), [client]);
70
76
 
71
77
  return (
72
- <UpstepContext.Provider value={{ client, feedItems, loadFeed, submit, vote, openSheet, identify, sheetRef }}>
78
+ <UpstepContext.Provider
79
+ value={{
80
+ client, feedItems, loadFeed, submit, vote, openSheet, identify, sheetRef,
81
+ accentColor: config.accentColor ?? DEFAULT_ACCENT,
82
+ theme: config.theme ?? "auto",
83
+ }}
84
+ >
73
85
  {children}
74
86
  </UpstepContext.Provider>
75
87
  );
@@ -5,8 +5,8 @@ import {
5
5
  TextInput,
6
6
  TouchableOpacity,
7
7
  StyleSheet,
8
- ScrollView,
9
8
  ActivityIndicator,
9
+ useColorScheme,
10
10
  } from "react-native";
11
11
  import BottomSheet, { BottomSheetView, BottomSheetScrollView } from "@gorhom/bottom-sheet";
12
12
  import type { Feedback, FeedbackType } from "@upstep/types";
@@ -15,10 +15,37 @@ import { useUpstep } from "./FeedbackContext";
15
15
  type Tab = "submit" | "feed";
16
16
 
17
17
  const TYPES: FeedbackType[] = ["BUG", "FEATURE", "GENERAL"];
18
+ const TYPE_LABELS: Record<FeedbackType, string> = {
19
+ BUG: "🐞 Bug", FEATURE: "✨ Feature", GENERAL: "💬 General",
20
+ };
21
+
22
+ interface Palette {
23
+ bg: string;
24
+ bgSoft: string;
25
+ text: string;
26
+ textSoft: string;
27
+ textFaint: string;
28
+ border: string;
29
+ handle: string;
30
+ }
31
+
32
+ const LIGHT: Palette = {
33
+ bg: "#ffffff", bgSoft: "#f6f5f2", text: "#1a1915",
34
+ textSoft: "#56544d", textFaint: "#9b9890", border: "#e8e6df", handle: "#dad7ce",
35
+ };
36
+ const DARK: Palette = {
37
+ bg: "#1c1b19", bgSoft: "#262522", text: "#f5f4f0",
38
+ textSoft: "#b4b1a8", textFaint: "#7d7a72", border: "#33312c", handle: "#48453f",
39
+ };
18
40
 
19
41
  export function FeedbackSheet() {
20
- const { feedItems, submit, vote, sheetRef } = useUpstep();
21
- const snapPoints = useMemo(() => ["75%", "95%"], []);
42
+ const { feedItems, submit, vote, sheetRef, accentColor, theme } = useUpstep();
43
+ const scheme = useColorScheme();
44
+ const isDark = theme === "dark" || (theme === "auto" && scheme === "dark");
45
+ const p = isDark ? DARK : LIGHT;
46
+ const accent = accentColor;
47
+
48
+ const snapPoints = useMemo(() => ["80%"], []);
22
49
  const [tab, setTab] = useState<Tab>("submit");
23
50
  const [content, setContent] = useState("");
24
51
  const [type, setType] = useState<FeedbackType>("GENERAL");
@@ -31,7 +58,7 @@ export function FeedbackSheet() {
31
58
  try {
32
59
  await submit(content.trim(), type);
33
60
  setContent("");
34
- setMsg("Thanks for your feedback!");
61
+ setMsg("Thanks — we got your feedback! 🎉");
35
62
  } catch {
36
63
  setMsg("Something went wrong. Please try again.");
37
64
  } finally {
@@ -45,25 +72,33 @@ export function FeedbackSheet() {
45
72
  index={-1}
46
73
  snapPoints={snapPoints}
47
74
  enablePanDownToClose
48
- backgroundStyle={styles.sheet}
49
- handleIndicatorStyle={styles.handle}
75
+ backgroundStyle={{ backgroundColor: p.bg, borderRadius: 24 }}
76
+ handleIndicatorStyle={{ backgroundColor: p.handle, width: 40, height: 4 }}
50
77
  >
51
78
  <BottomSheetView style={styles.header}>
52
- <Text style={styles.title}>Share feedback</Text>
79
+ <Text style={[styles.title, { color: p.text }]}>Share your feedback</Text>
80
+ <Text style={[styles.subtitle, { color: p.textFaint }]}>
81
+ Tell us what to build next, or vote on ideas.
82
+ </Text>
53
83
  </BottomSheetView>
54
84
 
55
- <View style={styles.tabs}>
56
- {(["submit", "feed"] as Tab[]).map((t) => (
57
- <TouchableOpacity
58
- key={t}
59
- onPress={() => setTab(t)}
60
- style={[styles.tab, tab === t && styles.activeTab]}
61
- >
62
- <Text style={[styles.tabText, tab === t && styles.activeTabText]}>
63
- {t === "submit" ? "Submit" : "Votes & Ideas"}
64
- </Text>
65
- </TouchableOpacity>
66
- ))}
85
+ {/* Segmented control */}
86
+ <View style={[styles.segment, { backgroundColor: p.bgSoft }]}>
87
+ {(["submit", "feed"] as Tab[]).map((t) => {
88
+ const active = tab === t;
89
+ return (
90
+ <TouchableOpacity
91
+ key={t}
92
+ onPress={() => setTab(t)}
93
+ activeOpacity={0.8}
94
+ style={[styles.segmentBtn, active && { backgroundColor: p.bg, shadowOpacity: 0.08 }]}
95
+ >
96
+ <Text style={[styles.segmentText, { color: active ? p.text : p.textSoft }]}>
97
+ {t === "submit" ? "Give feedback" : "Vote on ideas"}
98
+ </Text>
99
+ </TouchableOpacity>
100
+ );
101
+ })}
67
102
  </View>
68
103
 
69
104
  {tab === "submit" ? (
@@ -72,28 +107,36 @@ export function FeedbackSheet() {
72
107
  value={content}
73
108
  onChangeText={setContent}
74
109
  placeholder="What's on your mind?"
75
- placeholderTextColor="#9ca3af"
110
+ placeholderTextColor={p.textFaint}
76
111
  multiline
77
112
  maxLength={2000}
78
- style={styles.textarea}
113
+ style={[styles.textarea, { borderColor: p.border, backgroundColor: p.bgSoft, color: p.text }]}
79
114
  />
80
115
  <View style={styles.typeRow}>
81
- {TYPES.map((t) => (
82
- <TouchableOpacity
83
- key={t}
84
- onPress={() => setType(t)}
85
- style={[styles.typeBtn, type === t && styles.selectedTypeBtn]}
86
- >
87
- <Text style={[styles.typeBtnText, type === t && styles.selectedTypeBtnText]}>
88
- {t.charAt(0) + t.slice(1).toLowerCase()}
89
- </Text>
90
- </TouchableOpacity>
91
- ))}
116
+ {TYPES.map((t) => {
117
+ const active = type === t;
118
+ return (
119
+ <TouchableOpacity
120
+ key={t}
121
+ onPress={() => setType(t)}
122
+ activeOpacity={0.8}
123
+ style={[
124
+ styles.typeBtn,
125
+ { borderColor: active ? accent : p.border, backgroundColor: active ? accent : p.bg },
126
+ ]}
127
+ >
128
+ <Text style={[styles.typeBtnText, { color: active ? "#fff" : p.textSoft }]}>
129
+ {TYPE_LABELS[t]}
130
+ </Text>
131
+ </TouchableOpacity>
132
+ );
133
+ })}
92
134
  </View>
93
135
  <TouchableOpacity
94
136
  onPress={handleSubmit}
95
137
  disabled={loading || !content.trim()}
96
- style={[styles.submitBtn, (loading || !content.trim()) && styles.disabledBtn]}
138
+ activeOpacity={0.85}
139
+ style={[styles.submitBtn, { backgroundColor: accent }, (loading || !content.trim()) && styles.disabledBtn]}
97
140
  >
98
141
  {loading ? (
99
142
  <ActivityIndicator color="#fff" />
@@ -101,15 +144,18 @@ export function FeedbackSheet() {
101
144
  <Text style={styles.submitBtnText}>Send feedback</Text>
102
145
  )}
103
146
  </TouchableOpacity>
104
- {msg ? <Text style={styles.msgText}>{msg}</Text> : null}
147
+ {msg ? <Text style={[styles.msgText, { color: accent }]}>{msg}</Text> : null}
105
148
  </BottomSheetView>
106
149
  ) : (
107
150
  <BottomSheetScrollView contentContainerStyle={styles.body}>
108
151
  {feedItems.length === 0 ? (
109
- <Text style={styles.emptyText}>No feedback yet. Be the first!</Text>
152
+ <View style={styles.empty}>
153
+ <Text style={[styles.emptyTitle, { color: p.textSoft }]}>No ideas yet</Text>
154
+ <Text style={[styles.emptyText, { color: p.textFaint }]}>Be the first to share one.</Text>
155
+ </View>
110
156
  ) : (
111
157
  feedItems.map((f) => (
112
- <FeedItem key={f.id} item={f} onVote={vote} />
158
+ <FeedItem key={f.id} item={f} onVote={vote} accent={accent} p={p} />
113
159
  ))
114
160
  )}
115
161
  </BottomSheetScrollView>
@@ -118,62 +164,76 @@ export function FeedbackSheet() {
118
164
  );
119
165
  }
120
166
 
121
- function FeedItem({ item, onVote }: { item: Feedback; onVote: (id: string, v: "UP" | "DOWN") => void }) {
167
+ const BADGE_BG: Record<string, string> = { BUG: "#fdecec", FEATURE: "#e9f0fd" };
168
+ const BADGE_FG: Record<string, string> = { BUG: "#d6453d", FEATURE: "#3b76d6" };
169
+
170
+ function FeedItem({
171
+ item, onVote, accent, p,
172
+ }: {
173
+ item: Feedback;
174
+ onVote: (id: string, v: "UP" | "DOWN") => void;
175
+ accent: string;
176
+ p: Palette;
177
+ }) {
178
+ const voted = item.userVote === "UP";
122
179
  return (
123
- <View style={styles.feedItem}>
124
- <Text style={styles.feedContent}>{item.content}</Text>
125
- <View style={styles.feedMeta}>
126
- <View style={[styles.badge, item.type === "BUG" ? styles.badgeBug : item.type === "FEATURE" ? styles.badgeFeature : styles.badgeGeneral]}>
127
- <Text style={[styles.badgeText, item.type === "BUG" ? { color: "#dc2626" } : item.type === "FEATURE" ? { color: "#2563eb" } : { color: "#6b7280" }]}>
128
- {item.type}
180
+ <View style={[styles.feedItem, { borderColor: p.border, backgroundColor: p.bg }]}>
181
+ <TouchableOpacity
182
+ onPress={() => onVote(item.id, "UP")}
183
+ activeOpacity={0.8}
184
+ style={[
185
+ styles.vote,
186
+ { borderColor: voted ? accent : p.border, backgroundColor: voted ? `${accent}1a` : p.bgSoft },
187
+ ]}
188
+ >
189
+ <Text style={[styles.voteArrow, { color: accent }]}>▲</Text>
190
+ <Text style={[styles.voteCount, { color: p.text }]}>{item.upvotes}</Text>
191
+ </TouchableOpacity>
192
+ <View style={styles.feedMain}>
193
+ <Text style={[styles.feedContent, { color: p.text }]}>{item.content}</Text>
194
+ <View style={styles.feedMeta}>
195
+ <View style={[styles.badge, { backgroundColor: BADGE_BG[item.type] ?? p.bgSoft }]}>
196
+ <Text style={[styles.badgeText, { color: BADGE_FG[item.type] ?? p.textSoft }]}>{item.type}</Text>
197
+ </View>
198
+ <Text style={[styles.feedDate, { color: p.textFaint }]}>
199
+ {new Date(item.createdAt).toLocaleDateString()}
129
200
  </Text>
130
201
  </View>
131
- <Text style={styles.feedDate}>{new Date(item.createdAt).toLocaleDateString()}</Text>
132
- </View>
133
- <View style={styles.voteRow}>
134
- <TouchableOpacity style={styles.voteBtn} onPress={() => onVote(item.id, "UP")}>
135
- <Text style={styles.voteBtnText}>▲ {item.upvotes}</Text>
136
- </TouchableOpacity>
137
- <TouchableOpacity style={styles.voteBtn} onPress={() => onVote(item.id, "DOWN")}>
138
- <Text style={styles.voteBtnText}>▼ {item.downvotes}</Text>
139
- </TouchableOpacity>
140
202
  </View>
141
203
  </View>
142
204
  );
143
205
  }
144
206
 
145
207
  const styles = StyleSheet.create({
146
- sheet: { backgroundColor: "#fff", borderRadius: 16 },
147
- handle: { backgroundColor: "#d1d5db", width: 36, height: 4 },
148
- header: { paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: "#e5e7eb" },
149
- title: { fontSize: 16, fontWeight: "700", color: "#111827" },
150
- tabs: { flexDirection: "row", borderBottomWidth: 1, borderBottomColor: "#e5e7eb" },
151
- tab: { flex: 1, paddingVertical: 12, alignItems: "center", borderBottomWidth: 2, borderBottomColor: "transparent" },
152
- activeTab: { borderBottomColor: "#6366f1" },
153
- tabText: { fontSize: 13, fontWeight: "500", color: "#6b7280" },
154
- activeTabText: { color: "#6366f1" },
155
- body: { padding: 16 },
156
- textarea: { borderWidth: 1, borderColor: "#d1d5db", borderRadius: 8, padding: 10, fontSize: 14, minHeight: 100, textAlignVertical: "top", color: "#111827" },
157
- typeRow: { flexDirection: "row", gap: 8, marginVertical: 10 },
158
- typeBtn: { paddingVertical: 5, paddingHorizontal: 12, borderRadius: 9999, borderWidth: 1, borderColor: "#d1d5db" },
159
- selectedTypeBtn: { backgroundColor: "#6366f1", borderColor: "#6366f1" },
160
- typeBtnText: { fontSize: 12, color: "#374151" },
161
- selectedTypeBtnText: { color: "#fff" },
162
- submitBtn: { backgroundColor: "#6366f1", borderRadius: 8, padding: 12, alignItems: "center", marginTop: 8 },
208
+ header: { paddingHorizontal: 20, paddingTop: 4, paddingBottom: 4 },
209
+ title: { fontSize: 17, fontWeight: "700", letterSpacing: -0.2 },
210
+ subtitle: { fontSize: 12.5, marginTop: 3 },
211
+ segment: { flexDirection: "row", gap: 4, marginHorizontal: 20, marginTop: 16, padding: 4, borderRadius: 12 },
212
+ segmentBtn: {
213
+ flex: 1, paddingVertical: 9, alignItems: "center", borderRadius: 9,
214
+ shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowRadius: 3, shadowOpacity: 0,
215
+ },
216
+ segmentText: { fontSize: 13, fontWeight: "600" },
217
+ body: { padding: 20 },
218
+ textarea: { borderWidth: 1, borderRadius: 12, padding: 12, fontSize: 14, minHeight: 110, textAlignVertical: "top", lineHeight: 21 },
219
+ typeRow: { flexDirection: "row", gap: 7, marginVertical: 12 },
220
+ typeBtn: { flex: 1, paddingVertical: 9, alignItems: "center", borderRadius: 9, borderWidth: 1 },
221
+ typeBtnText: { fontSize: 12.5, fontWeight: "600" },
222
+ submitBtn: { borderRadius: 11, padding: 13, alignItems: "center", marginTop: 4 },
163
223
  disabledBtn: { opacity: 0.5 },
164
224
  submitBtnText: { color: "#fff", fontSize: 14, fontWeight: "600" },
165
- msgText: { textAlign: "center", fontSize: 13, color: "#6b7280", marginTop: 8 },
166
- emptyText: { textAlign: "center", color: "#9ca3af", fontSize: 14, paddingVertical: 32 },
167
- feedItem: { paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: "#f3f4f6" },
168
- feedContent: { fontSize: 14, color: "#1f2937", marginBottom: 6 },
169
- feedMeta: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
225
+ msgText: { textAlign: "center", fontSize: 13, fontWeight: "500", marginTop: 12 },
226
+ empty: { alignItems: "center", paddingVertical: 40 },
227
+ emptyTitle: { fontSize: 15, fontWeight: "600", marginBottom: 4 },
228
+ emptyText: { fontSize: 14 },
229
+ feedItem: { flexDirection: "row", gap: 12, padding: 14, borderWidth: 1, borderRadius: 14, marginBottom: 10 },
230
+ vote: { minWidth: 46, paddingVertical: 7, borderRadius: 11, borderWidth: 1, alignItems: "center", justifyContent: "center" },
231
+ voteArrow: { fontSize: 11, lineHeight: 13 },
232
+ voteCount: { fontSize: 14, fontWeight: "700", lineHeight: 17 },
233
+ feedMain: { flex: 1 },
234
+ feedContent: { fontSize: 13.5, lineHeight: 20, marginBottom: 8 },
235
+ feedMeta: { flexDirection: "row", alignItems: "center", gap: 7 },
170
236
  badge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 9999 },
171
- badgeBug: { backgroundColor: "#fee2e2" },
172
- badgeFeature: { backgroundColor: "#dbeafe" },
173
- badgeGeneral: { backgroundColor: "#f3f4f6" },
174
- badgeText: { fontSize: 11, fontWeight: "500" },
175
- feedDate: { fontSize: 11, color: "#9ca3af" },
176
- voteRow: { flexDirection: "row", gap: 8 },
177
- voteBtn: { flexDirection: "row", alignItems: "center", paddingHorizontal: 10, paddingVertical: 4, borderRadius: 9999, borderWidth: 1, borderColor: "#e5e7eb" },
178
- voteBtnText: { fontSize: 12, color: "#374151" },
237
+ badgeText: { fontSize: 10.5, fontWeight: "700", letterSpacing: 0.2 },
238
+ feedDate: { fontSize: 11 },
179
239
  });