@upstep/react-native 0.6.12 → 0.6.14

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 ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.6.14
4
+
5
+ - Show dashboard custom labels on feedback cards and detail views.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upstep/react-native",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
4
4
  "description": "Upstep feedback SDK for React Native",
5
5
  "homepage": "https://upstep.dev",
6
6
  "repository": {
@@ -19,14 +19,16 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@upstep/types": "0.5.4"
22
+ "@upstep/types": "0.5.5"
23
23
  },
24
24
  "peerDependencies": {
25
+ "@react-native-async-storage/async-storage": ">=1.18",
25
26
  "react": ">=18",
26
27
  "react-native": ">=0.73",
27
28
  "react-native-safe-area-context": ">=4"
28
29
  },
29
30
  "devDependencies": {
31
+ "@react-native-async-storage/async-storage": "^2.2.0",
30
32
  "@types/react": "^19.0.7",
31
33
  "@types/react-native": "^0.73.0",
32
34
  "react-native-safe-area-context": "^5.8.0",
@@ -280,6 +280,7 @@ function FeedCard({
280
280
  : <StatusDot status={item.status} />}
281
281
  <Text style={[styles.cardDate, { color: p.textFaint }]}>{fmtDate(item.createdAt)}</Text>
282
282
  </View>
283
+ <LabelBadges labels={item.labels} p={p} />
283
284
  </View>
284
285
 
285
286
  <Text style={[styles.chevron, { color: p.textFaint }]}>›</Text>
@@ -338,6 +339,7 @@ function FeedDetailBody({
338
339
  {item.status === "PENDING"
339
340
  ? <PendingBadge />
340
341
  : <StatusBadge status={item.status} p={p} />}
342
+ <LabelBadges labels={item.labels} p={p} />
341
343
  </View>
342
344
 
343
345
  <Text style={[styles.detailContent, { color: p.textSoft }]}>{item.content}</Text>
@@ -560,6 +562,21 @@ function StatusBadge({ status, p }: { status: string; p: Palette }) {
560
562
  );
561
563
  }
562
564
 
565
+ function LabelBadges({ labels, p }: { labels: Feedback["labels"]; p: Palette }) {
566
+ if (!labels?.length) return null;
567
+
568
+ return (
569
+ <View style={styles.labelBadges}>
570
+ {labels.map((label) => (
571
+ <View key={label.id} style={[styles.labelBadge, { backgroundColor: p.bgSoft, borderColor: label.color }]}>
572
+ <View style={[styles.labelDot, { backgroundColor: label.color }]} />
573
+ <Text style={[styles.labelBadgeText, { color: p.textSoft }]} numberOfLines={1}>{label.name}</Text>
574
+ </View>
575
+ ))}
576
+ </View>
577
+ );
578
+ }
579
+
563
580
  function fmtDate(iso: string) {
564
581
  return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" });
565
582
  }
@@ -641,12 +658,16 @@ const styles = StyleSheet.create({
641
658
  cardDesc: { fontSize: 12.5, lineHeight: 17, marginBottom: 6 },
642
659
  cardMeta: { flexDirection: "row", alignItems: "center", gap: 6 },
643
660
  cardDate: { fontSize: 11 },
661
+ labelBadges: { flexDirection: "row", flexWrap: "wrap", gap: 6, marginTop: 7 },
662
+ labelBadge: { flexDirection: "row", alignItems: "center", gap: 5, borderWidth: 1, borderRadius: 10, paddingHorizontal: 7, paddingVertical: 3, maxWidth: "100%" },
663
+ labelDot: { width: 6, height: 6, borderRadius: 3 },
664
+ labelBadgeText: { fontSize: 11, fontWeight: "500", flexShrink: 1 },
644
665
  chevron: { fontSize: 22, fontWeight: "300", marginLeft: 4 },
645
666
 
646
667
  // Detail
647
668
  detailPad: { paddingTop: 4 },
648
669
  detailTitle: { fontSize: 22, fontWeight: "700", letterSpacing: -0.4, marginBottom: 10, lineHeight: 28 },
649
- badgeRow: { flexDirection: "row", gap: 8, marginBottom: 16 },
670
+ badgeRow: { flexDirection: "row", flexWrap: "wrap", gap: 8, marginBottom: 16 },
650
671
  detailContent: { fontSize: 15, lineHeight: 23, marginBottom: 24 },
651
672
  upvoteBtn: {
652
673
  flexDirection: "row", alignItems: "center", gap: 8,
package/src/api.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import AsyncStorage from "@react-native-async-storage/async-storage";
1
2
  import type {
2
3
  Feedback,
3
4
  FeedbackListResponse,
@@ -7,15 +8,42 @@ import type {
7
8
  VoteValue,
8
9
  } from "@upstep/types";
9
10
 
11
+ const ANON_ID_KEY = "upstep-anonymous-id";
12
+
13
+ function generateId(): string {
14
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
15
+ }
16
+
10
17
  export class UpstepApiClient {
11
18
  private readonly baseUrl: string;
12
19
  private readonly apiKey: string;
13
20
  userId: string | undefined;
21
+ // Set synchronously to a throwaway id so there's always something to send;
22
+ // swapped for the persisted value (or backfilled into storage) once
23
+ // AsyncStorage resolves. Callers await anonymousIdReady first so requests
24
+ // always go out with the id that's actually persisted.
25
+ anonymousId: string;
26
+ private readonly anonymousIdReady: Promise<void>;
14
27
 
15
28
  constructor(config: UpstepConfig) {
16
29
  this.baseUrl = (config.baseUrl ?? "https://upstep.dev").replace(/\/$/, "");
17
30
  this.apiKey = config.apiKey;
18
31
  if (config.userId !== undefined) this.userId = config.userId;
32
+ this.anonymousId = generateId();
33
+ this.anonymousIdReady = this.loadAnonymousId();
34
+ }
35
+
36
+ private async loadAnonymousId(): Promise<void> {
37
+ try {
38
+ const stored = await AsyncStorage.getItem(ANON_ID_KEY);
39
+ if (stored) {
40
+ this.anonymousId = stored;
41
+ } else {
42
+ await AsyncStorage.setItem(ANON_ID_KEY, this.anonymousId);
43
+ }
44
+ } catch {
45
+ // storage unavailable - keep the in-memory id for this session
46
+ }
19
47
  }
20
48
 
21
49
  /** Update the end-user id at runtime (e.g. after the user logs in). */
@@ -28,20 +56,25 @@ export class UpstepApiClient {
28
56
  }
29
57
 
30
58
  async listFeedback(params?: { cursor?: string; limit?: number; sort?: "date" | "votes" }): Promise<FeedbackListResponse> {
59
+ await this.anonymousIdReady;
31
60
  const qs = new URLSearchParams();
32
61
  if (params?.cursor) qs.set("cursor", params.cursor);
33
62
  if (params?.limit) qs.set("limit", String(params.limit));
34
63
  if (params?.sort) qs.set("sort", params.sort);
35
64
  // Pass userId so the server can include the user's own pending items.
36
65
  if (this.userId) qs.set("endUserId", this.userId);
66
+ qs.set("anonymousId", this.anonymousId);
37
67
  const res = await fetch(`${this.baseUrl}/api/sdk/feedback?${qs}`, { headers: this.headers() });
38
68
  if (!res.ok) throw new Error(`Upstep: ${res.status}`);
39
69
  return res.json() as Promise<FeedbackListResponse>;
40
70
  }
41
71
 
42
72
  async getItem(feedbackId: string): Promise<FeedbackWithComments> {
43
- const qs = this.userId ? `?endUserId=${encodeURIComponent(this.userId)}` : "";
44
- const res = await fetch(`${this.baseUrl}/api/sdk/feedback/${feedbackId}${qs}`, {
73
+ await this.anonymousIdReady;
74
+ const qs = new URLSearchParams();
75
+ if (this.userId) qs.set("endUserId", this.userId);
76
+ qs.set("anonymousId", this.anonymousId);
77
+ const res = await fetch(`${this.baseUrl}/api/sdk/feedback/${feedbackId}?${qs}`, {
45
78
  headers: this.headers(),
46
79
  });
47
80
  if (!res.ok) throw new Error(`Upstep: ${res.status}`);
@@ -49,20 +82,26 @@ export class UpstepApiClient {
49
82
  }
50
83
 
51
84
  async submitFeedback(payload: SubmitFeedbackPayload): Promise<Feedback> {
85
+ await this.anonymousIdReady;
52
86
  const res = await fetch(`${this.baseUrl}/api/sdk/feedback`, {
53
87
  method: "POST",
54
88
  headers: this.headers(),
55
- body: JSON.stringify({ ...payload, endUserId: payload.endUserId ?? this.userId }),
89
+ body: JSON.stringify({
90
+ ...payload,
91
+ endUserId: payload.endUserId ?? this.userId,
92
+ anonymousId: payload.anonymousId ?? this.anonymousId,
93
+ }),
56
94
  });
57
95
  if (!res.ok) throw new Error(`Upstep: ${res.status}`);
58
96
  return res.json() as Promise<Feedback>;
59
97
  }
60
98
 
61
99
  async vote(feedbackId: string, value: VoteValue): Promise<void> {
100
+ await this.anonymousIdReady;
62
101
  const res = await fetch(`${this.baseUrl}/api/sdk/feedback/${feedbackId}/vote`, {
63
102
  method: "POST",
64
103
  headers: this.headers(),
65
- body: JSON.stringify({ value, endUserId: this.userId }),
104
+ body: JSON.stringify({ value, endUserId: this.userId, anonymousId: this.anonymousId }),
66
105
  });
67
106
  if (!res.ok) throw new Error(`Upstep: ${res.status}`);
68
107
  }
@@ -1,4 +0,0 @@
1
-
2
- > @upstep/react-native@0.6.12 type-check /Users/sandhyasandy/Dev/upstep/packages/sdk-react-native
3
- > tsc --noEmit
4
-