@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 +5 -0
- package/package.json +4 -2
- package/src/FeedbackSheet.tsx +22 -1
- package/src/api.ts +43 -4
- package/.turbo/turbo-type-check.log +0 -4
package/CHANGELOG.md
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upstep/react-native",
|
|
3
|
-
"version": "0.6.
|
|
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.
|
|
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",
|
package/src/FeedbackSheet.tsx
CHANGED
|
@@ -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
|
-
|
|
44
|
-
const
|
|
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({
|
|
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
|
}
|