@upstep/react-native 0.6.3 → 0.6.7

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.6.3",
3
+ "version": "0.6.7",
4
4
  "description": "Upstep feedback SDK for React Native",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -6,6 +6,7 @@ import React, {
6
6
  useCallback,
7
7
  type ReactNode,
8
8
  } from "react";
9
+ import { LayoutAnimation, Platform, UIManager } from "react-native";
9
10
  import type {
10
11
  Feedback,
11
12
  FeedbackType,
@@ -15,6 +16,12 @@ import type {
15
16
  } from "@upstep/types";
16
17
  import { UpstepApiClient } from "./api";
17
18
 
19
+ // Enable LayoutAnimation on Android
20
+ if (Platform.OS === "android") {
21
+ const um = UIManager as unknown as { setLayoutAnimationEnabledExperimental?: (v: boolean) => void };
22
+ um.setLayoutAnimationEnabledExperimental?.(true);
23
+ }
24
+
18
25
  const DEFAULT_ACCENT = "#D97757";
19
26
 
20
27
  interface UpstepContextValue {
@@ -74,6 +81,22 @@ export function FeedbackProvider({
74
81
  );
75
82
 
76
83
  const vote = useCallback(async (feedbackId: string, value: VoteValue) => {
84
+ // Optimistic update: flip vote + re-sort with a smooth animation
85
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
86
+ setFeedItems((prev) => {
87
+ const updated = prev.map((f) => {
88
+ if (f.id !== feedbackId) return f;
89
+ const wasVoted = f.userVote === "UP";
90
+ return {
91
+ ...f,
92
+ upvotes: wasVoted ? f.upvotes - 1 : f.upvotes + 1,
93
+ userVote: wasVoted ? null : ("UP" as VoteValue),
94
+ };
95
+ });
96
+ return [...updated].sort((a, b) => b.upvotes - a.upvotes);
97
+ });
98
+
99
+ // Confirm with server, then silently sync
77
100
  await client.vote(feedbackId, value);
78
101
  await loadFeed();
79
102
  }, [client, loadFeed]);
@@ -66,6 +66,7 @@ export function FeedbackSheet() {
66
66
  const p = isDark ? DARK : LIGHT;
67
67
 
68
68
  const [nav, setNav] = useState<Nav>({ screen: "list" });
69
+ const [listTab, setListTab] = useState<"open" | "done">("open");
69
70
  const insets = useSafeAreaInsets();
70
71
 
71
72
  // Refresh list every time the modal opens
@@ -135,6 +136,30 @@ export function FeedbackSheet() {
135
136
  )}
136
137
  </View>
137
138
 
139
+ {/* Open / Completed tabs — only on list screen */}
140
+ {nav.screen === "list" && (
141
+ <View style={[styles.listTabBar, { borderBottomColor: p.border }]}>
142
+ <TouchableOpacity
143
+ onPress={() => setListTab("open")}
144
+ style={[styles.listTabBtn, listTab === "open" && { borderBottomColor: accentColor }]}
145
+ activeOpacity={0.7}
146
+ >
147
+ <Text style={[styles.listTabText, { color: listTab === "open" ? accentColor : p.textFaint }]}>
148
+ Open
149
+ </Text>
150
+ </TouchableOpacity>
151
+ <TouchableOpacity
152
+ onPress={() => setListTab("done")}
153
+ style={[styles.listTabBtn, listTab === "done" && { borderBottomColor: accentColor }]}
154
+ activeOpacity={0.7}
155
+ >
156
+ <Text style={[styles.listTabText, { color: listTab === "done" ? accentColor : p.textFaint }]}>
157
+ Completed
158
+ </Text>
159
+ </TouchableOpacity>
160
+ </View>
161
+ )}
162
+
138
163
  {/* Scrollable body */}
139
164
  <ScrollView
140
165
  style={styles.scroll}
@@ -146,6 +171,7 @@ export function FeedbackSheet() {
146
171
  <FeedListBody
147
172
  p={p}
148
173
  accent={accentColor}
174
+ tab={listTab}
149
175
  onSelect={(id) => setNav({ screen: "detail", feedbackId: id })}
150
176
  />
151
177
  )}
@@ -189,12 +215,16 @@ export function FeedbackSheet() {
189
215
  // ─── FeedListBody ─────────────────────────────────────────────────────────────
190
216
 
191
217
  function FeedListBody({
192
- p, accent, onSelect,
218
+ p, accent, tab, onSelect,
193
219
  }: {
194
- p: Palette; accent: string; onSelect: (id: string) => void;
220
+ p: Palette; accent: string; tab: "open" | "done"; onSelect: (id: string) => void;
195
221
  }) {
196
222
  const { feedItems, feedLoading, vote } = useUpstep();
197
223
 
224
+ const displayItems = tab === "done"
225
+ ? feedItems.filter((f) => f.status === "DONE")
226
+ : feedItems.filter((f) => f.status === "OPEN" || f.status === "IN_PROGRESS" || f.status === "PENDING");
227
+
198
228
  if (feedLoading) {
199
229
  return (
200
230
  <View style={styles.centered}>
@@ -203,11 +233,13 @@ function FeedListBody({
203
233
  );
204
234
  }
205
235
 
206
- if (feedItems.length === 0) {
236
+ if (displayItems.length === 0) {
207
237
  return (
208
238
  <View style={styles.centered}>
209
239
  <Text style={[styles.emptyText, { color: p.textFaint }]}>
210
- No feedback yet. Be the first to add one.
240
+ {tab === "done"
241
+ ? "No completed items yet."
242
+ : "No open feedback yet. Be the first to add one."}
211
243
  </Text>
212
244
  </View>
213
245
  );
@@ -215,7 +247,7 @@ function FeedListBody({
215
247
 
216
248
  return (
217
249
  <>
218
- {feedItems.map((f) => (
250
+ {displayItems.map((f) => (
219
251
  <FeedCard
220
252
  key={f.id}
221
253
  item={f}
@@ -271,7 +303,9 @@ function FeedCard({
271
303
  )}
272
304
  <View style={styles.cardMeta}>
273
305
  <TypeBadge type={item.type} p={p} />
274
- <StatusDot status={item.status} />
306
+ {item.status === "PENDING"
307
+ ? <PendingBadge />
308
+ : <StatusDot status={item.status} />}
275
309
  <Text style={[styles.cardDate, { color: p.textFaint }]}>{fmtDate(item.createdAt)}</Text>
276
310
  </View>
277
311
  </View>
@@ -329,7 +363,9 @@ function FeedDetailBody({
329
363
  )}
330
364
  <View style={styles.badgeRow}>
331
365
  <TypeBadge type={item.type} p={p} />
332
- <StatusBadge status={item.status} p={p} />
366
+ {item.status === "PENDING"
367
+ ? <PendingBadge />
368
+ : <StatusBadge status={item.status} p={p} />}
333
369
  </View>
334
370
 
335
371
  <Text style={[styles.detailContent, { color: p.textSoft }]}>{item.content}</Text>
@@ -377,7 +413,7 @@ function CommentBubble({ comment, p, accent }: { comment: Comment; p: Palette; a
377
413
  <View style={styles.commentMeta}>
378
414
  <View style={[styles.ownerBadge, { backgroundColor: accent + "20" }]}>
379
415
  <Text style={[styles.ownerBadgeText, { color: accent }]}>
380
- {comment.authorName ?? "Team"}
416
+ Developer
381
417
  </Text>
382
418
  </View>
383
419
  <Text style={[styles.commentDate, { color: p.textFaint }]}>{fmtDate(comment.createdAt)}</Text>
@@ -517,6 +553,15 @@ const TYPE_FG: Record<FeedbackType, string> = {
517
553
  BUG: "#d6453d", FEATURE: "#3b76d6", GENERAL: "",
518
554
  };
519
555
 
556
+ function PendingBadge() {
557
+ return (
558
+ <View style={styles.pendingBadge}>
559
+ <View style={styles.pendingDot} />
560
+ <Text style={styles.pendingBadgeText}>Pending review</Text>
561
+ </View>
562
+ );
563
+ }
564
+
520
565
  function TypeBadge({ type, p }: { type: FeedbackType; p: Palette }) {
521
566
  return (
522
567
  <View style={[styles.badge, { backgroundColor: TYPE_BG[type] || p.bgSoft }]}>
@@ -593,6 +638,23 @@ const styles = StyleSheet.create({
593
638
  newBtn: { borderRadius: 20, paddingHorizontal: 14, paddingVertical: 7 },
594
639
  newBtnText: { color: "#fff", fontSize: 13, fontWeight: "700" },
595
640
 
641
+ // List tab bar
642
+ listTabBar: {
643
+ flexDirection: "row",
644
+ borderBottomWidth: StyleSheet.hairlineWidth,
645
+ },
646
+ listTabBtn: {
647
+ flex: 1,
648
+ alignItems: "center",
649
+ paddingVertical: 11,
650
+ borderBottomWidth: 2,
651
+ borderBottomColor: "transparent",
652
+ },
653
+ listTabText: {
654
+ fontSize: 13,
655
+ fontWeight: "600",
656
+ },
657
+
596
658
  // Scroll — flex: 1 fills all remaining space after the fixed header
597
659
  scroll: { flex: 1 },
598
660
  scrollContent: { flexGrow: 1, padding: 16, paddingBottom: 32 },
@@ -676,6 +738,30 @@ const styles = StyleSheet.create({
676
738
  successTitle: { fontSize: 20, fontWeight: "700", textAlign: "center", marginBottom: 6 },
677
739
  successSub: { fontSize: 14, textAlign: "center" },
678
740
 
741
+ // Pending badge
742
+ pendingBadge: {
743
+ flexDirection: "row",
744
+ alignItems: "center",
745
+ gap: 4,
746
+ backgroundColor: "#fff7ed",
747
+ borderWidth: 1,
748
+ borderColor: "#fed7aa",
749
+ borderRadius: 6,
750
+ paddingHorizontal: 6,
751
+ paddingVertical: 2,
752
+ },
753
+ pendingDot: {
754
+ width: 5,
755
+ height: 5,
756
+ borderRadius: 3,
757
+ backgroundColor: "#ea580c",
758
+ },
759
+ pendingBadgeText: {
760
+ fontSize: 10,
761
+ fontWeight: "700",
762
+ color: "#ea580c",
763
+ },
764
+
679
765
  // Branding
680
766
  poweredBy: {
681
767
  alignItems: "center",
package/src/api.ts CHANGED
@@ -40,7 +40,8 @@ export class UpstepApiClient {
40
40
  }
41
41
 
42
42
  async getItem(feedbackId: string): Promise<FeedbackWithComments> {
43
- const res = await fetch(`${this.baseUrl}/api/sdk/feedback/${feedbackId}`, {
43
+ const qs = this.userId ? `?endUserId=${encodeURIComponent(this.userId)}` : "";
44
+ const res = await fetch(`${this.baseUrl}/api/sdk/feedback/${feedbackId}${qs}`, {
44
45
  headers: this.headers(),
45
46
  });
46
47
  if (!res.ok) throw new Error(`Upstep: ${res.status}`);