@umituz/react-native-settings 5.3.41 → 5.3.43
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/dist/domains/feedback/domain/entities/FeatureRequestEntity.d.ts +0 -22
- package/dist/domains/feedback/infrastructure/useFeatureRequests.d.ts +21 -0
- package/dist/domains/feedback/presentation/screens/FeatureRequestScreen.d.ts +1 -11
- package/dist/presentation/hooks/useSettingsScreenConfig.d.ts +0 -2
- package/dist/presentation/screens/types/UserFeatureConfig.d.ts +0 -3
- package/package.json +1 -1
- package/src/domains/feedback/domain/entities/FeatureRequestEntity.ts +0 -25
- package/src/domains/feedback/infrastructure/useFeatureRequests.ts +219 -0
- package/src/domains/feedback/presentation/screens/FeatureRequestScreen.tsx +53 -308
- package/src/presentation/hooks/useSettingsScreenConfig.ts +2 -13
- package/src/presentation/navigation/hooks/useSettingsScreens.ts +0 -3
- package/src/presentation/screens/types/UserFeatureConfig.ts +0 -3
|
@@ -18,25 +18,3 @@ export interface FeatureRequestItem {
|
|
|
18
18
|
createdAt: string;
|
|
19
19
|
updatedAt: string;
|
|
20
20
|
}
|
|
21
|
-
export interface FeatureRequestVote {
|
|
22
|
-
type: VoteType;
|
|
23
|
-
votedAt: string;
|
|
24
|
-
}
|
|
25
|
-
export interface FeatureRequestSubmitData {
|
|
26
|
-
title: string;
|
|
27
|
-
description: string;
|
|
28
|
-
type: string;
|
|
29
|
-
rating?: number;
|
|
30
|
-
}
|
|
31
|
-
export interface FeatureRequestDataProvider {
|
|
32
|
-
/** Fetch all feature requests */
|
|
33
|
-
fetchRequests: () => Promise<FeatureRequestItem[]>;
|
|
34
|
-
/** Fetch current user's votes */
|
|
35
|
-
fetchUserVotes: (userId: string) => Promise<Record<string, VoteType>>;
|
|
36
|
-
/** Vote on a feature request */
|
|
37
|
-
onVote: (requestId: string, userId: string, type: VoteType) => Promise<void>;
|
|
38
|
-
/** Submit a new feature request */
|
|
39
|
-
onSubmit: (data: FeatureRequestSubmitData) => Promise<void>;
|
|
40
|
-
/** Current user ID (can be anonymous UID) */
|
|
41
|
-
userId: string | null;
|
|
42
|
-
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFeatureRequests Hook
|
|
3
|
+
* Internal hook — fetches, votes, submits feature requests via Firestore
|
|
4
|
+
* Works with both authenticated and anonymous users
|
|
5
|
+
*/
|
|
6
|
+
import type { FeatureRequestItem, VoteType } from "../domain/entities/FeatureRequestEntity";
|
|
7
|
+
export interface UseFeatureRequestsResult {
|
|
8
|
+
requests: FeatureRequestItem[];
|
|
9
|
+
userVotes: Record<string, VoteType>;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
vote: (requestId: string, type: VoteType) => Promise<void>;
|
|
12
|
+
submitRequest: (data: {
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
type: string;
|
|
16
|
+
rating?: number;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
reload: () => Promise<void>;
|
|
19
|
+
userId: string | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function useFeatureRequests(): UseFeatureRequestsResult;
|
|
@@ -1,12 +1,2 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
|
|
3
|
-
interface FeatureRequestScreenProps {
|
|
4
|
-
config?: {
|
|
5
|
-
translations?: Record<string, any>;
|
|
6
|
-
dataProvider?: FeatureRequestDataProvider;
|
|
7
|
-
onSubmit?: (data: any) => Promise<void>;
|
|
8
|
-
};
|
|
9
|
-
texts: any;
|
|
10
|
-
}
|
|
11
|
-
export declare const FeatureRequestScreen: React.FC<FeatureRequestScreenProps>;
|
|
12
|
-
export {};
|
|
2
|
+
export declare const FeatureRequestScreen: React.FC<any>;
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { SettingsConfig, SettingsTranslations } from "../screens/types";
|
|
9
9
|
import type { FeedbackFormData } from "../utils/config-creators";
|
|
10
|
-
import type { FeatureRequestDataProvider } from "../../domains/feedback/domain/entities/FeatureRequestEntity";
|
|
11
10
|
import type { AppInfo, FAQData, UserProfileDisplay, AdditionalScreen, AccountConfig } from "../navigation/types";
|
|
12
11
|
export interface SettingsFeatures {
|
|
13
12
|
notifications?: boolean;
|
|
@@ -27,7 +26,6 @@ export interface UseSettingsScreenConfigParams {
|
|
|
27
26
|
faqData?: FAQData;
|
|
28
27
|
isPremium: boolean;
|
|
29
28
|
onFeedbackSubmit: (data: FeedbackFormData) => Promise<void>;
|
|
30
|
-
featureRequestDataProvider?: FeatureRequestDataProvider;
|
|
31
29
|
additionalScreens?: AdditionalScreen[];
|
|
32
30
|
features?: SettingsFeatures;
|
|
33
31
|
translations?: SettingsTranslations;
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { FeatureVisibility } from "./BaseTypes";
|
|
6
6
|
import type { FeedbackType } from "../../../domains/feedback/domain/entities/FeedbackEntity";
|
|
7
|
-
import type { FeatureRequestDataProvider } from "../../../domains/feedback/domain/entities/FeatureRequestEntity";
|
|
8
7
|
import type { FAQCategory } from "../../../domains/faqs/domain/entities/FAQEntity";
|
|
9
8
|
import type { SettingsStackParamList } from "../../navigation/types";
|
|
10
9
|
/**
|
|
@@ -47,8 +46,6 @@ export interface FeedbackConfig {
|
|
|
47
46
|
}) => Promise<void>;
|
|
48
47
|
/** Custom handler to open feedback screen (overrides default modal) */
|
|
49
48
|
onPress?: () => void;
|
|
50
|
-
/** Data provider for feature request screen (fetching, voting, submitting) */
|
|
51
|
-
dataProvider?: FeatureRequestDataProvider;
|
|
52
49
|
}
|
|
53
50
|
/**
|
|
54
51
|
* FAQ Settings Configuration
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.43",
|
|
4
4
|
"description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -21,28 +21,3 @@ export interface FeatureRequestItem {
|
|
|
21
21
|
createdAt: string;
|
|
22
22
|
updatedAt: string;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
export interface FeatureRequestVote {
|
|
26
|
-
type: VoteType;
|
|
27
|
-
votedAt: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface FeatureRequestSubmitData {
|
|
31
|
-
title: string;
|
|
32
|
-
description: string;
|
|
33
|
-
type: string;
|
|
34
|
-
rating?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface FeatureRequestDataProvider {
|
|
38
|
-
/** Fetch all feature requests */
|
|
39
|
-
fetchRequests: () => Promise<FeatureRequestItem[]>;
|
|
40
|
-
/** Fetch current user's votes */
|
|
41
|
-
fetchUserVotes: (userId: string) => Promise<Record<string, VoteType>>;
|
|
42
|
-
/** Vote on a feature request */
|
|
43
|
-
onVote: (requestId: string, userId: string, type: VoteType) => Promise<void>;
|
|
44
|
-
/** Submit a new feature request */
|
|
45
|
-
onSubmit: (data: FeatureRequestSubmitData) => Promise<void>;
|
|
46
|
-
/** Current user ID (can be anonymous UID) */
|
|
47
|
-
userId: string | null;
|
|
48
|
-
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFeatureRequests Hook
|
|
3
|
+
* Internal hook — fetches, votes, submits feature requests via Firestore
|
|
4
|
+
* Works with both authenticated and anonymous users
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
8
|
+
import { Platform } from "react-native";
|
|
9
|
+
import { useAuth } from "@umituz/react-native-auth";
|
|
10
|
+
import {
|
|
11
|
+
collection,
|
|
12
|
+
doc,
|
|
13
|
+
getDocs,
|
|
14
|
+
getDoc,
|
|
15
|
+
setDoc,
|
|
16
|
+
addDoc,
|
|
17
|
+
updateDoc,
|
|
18
|
+
deleteDoc,
|
|
19
|
+
increment,
|
|
20
|
+
orderBy,
|
|
21
|
+
query,
|
|
22
|
+
serverTimestamp,
|
|
23
|
+
} from "firebase/firestore";
|
|
24
|
+
import { getFirestore } from "@umituz/react-native-firebase";
|
|
25
|
+
import type {
|
|
26
|
+
FeatureRequestItem,
|
|
27
|
+
FeatureRequestStatus,
|
|
28
|
+
VoteType,
|
|
29
|
+
} from "../domain/entities/FeatureRequestEntity";
|
|
30
|
+
|
|
31
|
+
const COLLECTION = "feature_requests";
|
|
32
|
+
|
|
33
|
+
export interface UseFeatureRequestsResult {
|
|
34
|
+
requests: FeatureRequestItem[];
|
|
35
|
+
userVotes: Record<string, VoteType>;
|
|
36
|
+
isLoading: boolean;
|
|
37
|
+
vote: (requestId: string, type: VoteType) => Promise<void>;
|
|
38
|
+
submitRequest: (data: { title: string; description: string; type: string; rating?: number }) => Promise<void>;
|
|
39
|
+
reload: () => Promise<void>;
|
|
40
|
+
userId: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useFeatureRequests(): UseFeatureRequestsResult {
|
|
44
|
+
const { user } = useAuth();
|
|
45
|
+
const userId = user?.uid ?? null;
|
|
46
|
+
|
|
47
|
+
const [requests, setRequests] = useState<FeatureRequestItem[]>([]);
|
|
48
|
+
const [userVotes, setUserVotes] = useState<Record<string, VoteType>>({});
|
|
49
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
50
|
+
|
|
51
|
+
// Ref to avoid stale closure in vote()
|
|
52
|
+
const userVotesRef = useRef(userVotes);
|
|
53
|
+
userVotesRef.current = userVotes;
|
|
54
|
+
|
|
55
|
+
// Guard against rapid double-tap on the same request
|
|
56
|
+
const votingInProgress = useRef(new Set<string>());
|
|
57
|
+
|
|
58
|
+
const fetchAll = useCallback(async () => {
|
|
59
|
+
const db = getFirestore();
|
|
60
|
+
if (!db) {
|
|
61
|
+
setIsLoading(false);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
setIsLoading(true);
|
|
67
|
+
|
|
68
|
+
const q = query(collection(db, COLLECTION), orderBy("votes", "desc"));
|
|
69
|
+
const snapshot = await getDocs(q);
|
|
70
|
+
|
|
71
|
+
const items: FeatureRequestItem[] = snapshot.docs.map((d) => {
|
|
72
|
+
const data = d.data();
|
|
73
|
+
return {
|
|
74
|
+
id: d.id,
|
|
75
|
+
title: data.title ?? "",
|
|
76
|
+
description: data.description ?? "",
|
|
77
|
+
type: data.type ?? "feature_request",
|
|
78
|
+
status: (data.status ?? "pending") as FeatureRequestStatus,
|
|
79
|
+
votes: data.votes ?? 0,
|
|
80
|
+
commentCount: data.commentCount ?? 0,
|
|
81
|
+
createdBy: data.createdBy ?? "",
|
|
82
|
+
isAnonymous: data.isAnonymous ?? false,
|
|
83
|
+
platform: data.platform ?? "unknown",
|
|
84
|
+
createdAt: data.createdAt?.toDate?.()?.toISOString?.() ?? new Date().toISOString(),
|
|
85
|
+
updatedAt: data.updatedAt?.toDate?.()?.toISOString?.() ?? new Date().toISOString(),
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
setRequests(items);
|
|
90
|
+
|
|
91
|
+
// Fetch user votes in parallel (not N+1 sequential)
|
|
92
|
+
if (userId && snapshot.docs.length > 0) {
|
|
93
|
+
const votePromises = snapshot.docs.map(async (reqDoc) => {
|
|
94
|
+
const voteRef = doc(db, COLLECTION, reqDoc.id, "votes", userId);
|
|
95
|
+
const voteSnap = await getDoc(voteRef);
|
|
96
|
+
if (voteSnap.exists()) {
|
|
97
|
+
return [reqDoc.id, voteSnap.data().type as VoteType] as const;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const results = await Promise.all(votePromises);
|
|
103
|
+
const votes: Record<string, VoteType> = {};
|
|
104
|
+
for (const result of results) {
|
|
105
|
+
if (result) votes[result[0]] = result[1];
|
|
106
|
+
}
|
|
107
|
+
setUserVotes(votes);
|
|
108
|
+
} else {
|
|
109
|
+
setUserVotes({});
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (__DEV__) console.warn("[useFeatureRequests] Load failed:", error);
|
|
113
|
+
} finally {
|
|
114
|
+
setIsLoading(false);
|
|
115
|
+
}
|
|
116
|
+
}, [userId]);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
fetchAll();
|
|
120
|
+
}, [fetchAll]);
|
|
121
|
+
|
|
122
|
+
const vote = useCallback(async (requestId: string, type: VoteType) => {
|
|
123
|
+
if (!userId) return;
|
|
124
|
+
|
|
125
|
+
// Prevent rapid double-tap on the same request
|
|
126
|
+
if (votingInProgress.current.has(requestId)) return;
|
|
127
|
+
votingInProgress.current.add(requestId);
|
|
128
|
+
|
|
129
|
+
const db = getFirestore();
|
|
130
|
+
if (!db) {
|
|
131
|
+
votingInProgress.current.delete(requestId);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Read latest vote from ref (not stale closure)
|
|
136
|
+
const previousVote = userVotesRef.current[requestId] as VoteType | undefined;
|
|
137
|
+
const isUndo = previousVote === type;
|
|
138
|
+
|
|
139
|
+
// Optimistic UI
|
|
140
|
+
setUserVotes((prev) => {
|
|
141
|
+
const next = { ...prev };
|
|
142
|
+
if (isUndo) { delete next[requestId]; } else { next[requestId] = type; }
|
|
143
|
+
return next;
|
|
144
|
+
});
|
|
145
|
+
setRequests((prev) =>
|
|
146
|
+
prev.map((r) => {
|
|
147
|
+
if (r.id !== requestId) return r;
|
|
148
|
+
let delta = 0;
|
|
149
|
+
if (isUndo) delta = type === "up" ? -1 : 1;
|
|
150
|
+
else if (previousVote) delta = type === "up" ? 2 : -2;
|
|
151
|
+
else delta = type === "up" ? 1 : -1;
|
|
152
|
+
return { ...r, votes: r.votes + delta };
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const voteRef = doc(db, COLLECTION, requestId, "votes", userId);
|
|
158
|
+
const requestRef = doc(db, COLLECTION, requestId);
|
|
159
|
+
const existing = await getDoc(voteRef);
|
|
160
|
+
|
|
161
|
+
if (existing.exists()) {
|
|
162
|
+
const prev = existing.data().type as VoteType;
|
|
163
|
+
if (prev === type) {
|
|
164
|
+
await deleteDoc(voteRef);
|
|
165
|
+
await updateDoc(requestRef, { votes: increment(type === "up" ? -1 : 1) });
|
|
166
|
+
} else {
|
|
167
|
+
await setDoc(voteRef, { type, votedAt: serverTimestamp() });
|
|
168
|
+
await updateDoc(requestRef, { votes: increment(type === "up" ? 2 : -2) });
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
await setDoc(voteRef, { type, votedAt: serverTimestamp() });
|
|
172
|
+
await updateDoc(requestRef, { votes: increment(type === "up" ? 1 : -1) });
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (__DEV__) console.warn("[useFeatureRequests] Vote failed:", error);
|
|
176
|
+
// Rollback using previous known state
|
|
177
|
+
setUserVotes((prev) => {
|
|
178
|
+
const next = { ...prev };
|
|
179
|
+
if (previousVote) next[requestId] = previousVote; else delete next[requestId];
|
|
180
|
+
return next;
|
|
181
|
+
});
|
|
182
|
+
fetchAll();
|
|
183
|
+
} finally {
|
|
184
|
+
votingInProgress.current.delete(requestId);
|
|
185
|
+
}
|
|
186
|
+
}, [userId, fetchAll]);
|
|
187
|
+
|
|
188
|
+
const submitRequest = useCallback(async (data: { title: string; description: string; type: string; rating?: number }) => {
|
|
189
|
+
const db = getFirestore();
|
|
190
|
+
if (!db) throw new Error("Firestore not available");
|
|
191
|
+
if (!userId) throw new Error("User not authenticated");
|
|
192
|
+
|
|
193
|
+
// Create the feature request
|
|
194
|
+
const docRef = await addDoc(collection(db, COLLECTION), {
|
|
195
|
+
title: data.title,
|
|
196
|
+
description: data.description,
|
|
197
|
+
type: data.type || "feature_request",
|
|
198
|
+
status: "pending",
|
|
199
|
+
votes: 1,
|
|
200
|
+
commentCount: 0,
|
|
201
|
+
createdBy: userId,
|
|
202
|
+
isAnonymous: user?.isAnonymous ?? false,
|
|
203
|
+
platform: Platform.OS,
|
|
204
|
+
rating: data.rating ?? null,
|
|
205
|
+
createdAt: serverTimestamp(),
|
|
206
|
+
updatedAt: serverTimestamp(),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Create the creator's upvote doc so votes count matches reality
|
|
210
|
+
await setDoc(doc(db, COLLECTION, docRef.id, "votes", userId), {
|
|
211
|
+
type: "up" as VoteType,
|
|
212
|
+
votedAt: serverTimestamp(),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await fetchAll();
|
|
216
|
+
}, [userId, user?.isAnonymous, fetchAll]);
|
|
217
|
+
|
|
218
|
+
return { requests, userVotes, isLoading, vote, submitRequest, reload: fetchAll, userId };
|
|
219
|
+
}
|
|
@@ -1,43 +1,25 @@
|
|
|
1
|
-
import React, { useState,
|
|
1
|
+
import React, { useState, useCallback } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
StyleSheet,
|
|
5
5
|
TouchableOpacity,
|
|
6
6
|
ScrollView,
|
|
7
|
-
FlatList,
|
|
8
7
|
ActivityIndicator,
|
|
9
8
|
} from "react-native";
|
|
10
9
|
import {
|
|
11
10
|
AtomicText,
|
|
12
11
|
AtomicIcon,
|
|
13
|
-
AtomicButton,
|
|
14
12
|
} from "@umituz/react-native-design-system/atoms";
|
|
15
13
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
16
14
|
import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
|
|
17
15
|
import { FeedbackModal } from "../components/FeedbackModal";
|
|
18
|
-
import
|
|
19
|
-
import type {
|
|
20
|
-
FeatureRequestItem,
|
|
21
|
-
FeatureRequestDataProvider,
|
|
22
|
-
VoteType,
|
|
23
|
-
} from "../../domain/entities/FeatureRequestEntity";
|
|
16
|
+
import { useFeatureRequests } from "../../infrastructure/useFeatureRequests";
|
|
17
|
+
import type { FeatureRequestItem, VoteType } from "../../domain/entities/FeatureRequestEntity";
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
config?: {
|
|
27
|
-
translations?: Record<string, any>;
|
|
28
|
-
dataProvider?: FeatureRequestDataProvider;
|
|
29
|
-
onSubmit?: (data: any) => Promise<void>;
|
|
30
|
-
};
|
|
31
|
-
texts: any;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ config, texts }) => {
|
|
19
|
+
export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
|
|
35
20
|
const tokens = useAppDesignTokens();
|
|
36
|
-
const
|
|
21
|
+
const { requests, userVotes, isLoading, vote, submitRequest, userId } = useFeatureRequests();
|
|
37
22
|
|
|
38
|
-
const [requests, setRequests] = useState<FeatureRequestItem[]>([]);
|
|
39
|
-
const [userVotes, setUserVotes] = useState<Record<string, VoteType>>({});
|
|
40
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
41
23
|
const [activeTab, setActiveTab] = useState<'all' | 'my' | 'roadmap'>('all');
|
|
42
24
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
43
25
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
@@ -63,101 +45,22 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
63
45
|
dismissed: t.status?.dismissed || "Dismissed",
|
|
64
46
|
};
|
|
65
47
|
|
|
66
|
-
const loadData = useCallback(async () => {
|
|
67
|
-
if (!dataProvider) {
|
|
68
|
-
setIsLoading(false);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
setIsLoading(true);
|
|
74
|
-
const [fetchedRequests, fetchedVotes] = await Promise.all([
|
|
75
|
-
dataProvider.fetchRequests(),
|
|
76
|
-
dataProvider.userId
|
|
77
|
-
? dataProvider.fetchUserVotes(dataProvider.userId)
|
|
78
|
-
: Promise.resolve({} as Record<string, VoteType>),
|
|
79
|
-
]);
|
|
80
|
-
setRequests(fetchedRequests);
|
|
81
|
-
setUserVotes(fetchedVotes);
|
|
82
|
-
} catch (error) {
|
|
83
|
-
if (__DEV__) console.warn("[FeatureRequestScreen] Failed to load data:", error);
|
|
84
|
-
} finally {
|
|
85
|
-
setIsLoading(false);
|
|
86
|
-
}
|
|
87
|
-
}, [dataProvider]);
|
|
88
|
-
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
loadData();
|
|
91
|
-
}, [loadData]);
|
|
92
|
-
|
|
93
|
-
const handleVote = useCallback(async (id: string, type: VoteType) => {
|
|
94
|
-
if (!dataProvider?.userId || !dataProvider.onVote) return;
|
|
95
|
-
|
|
96
|
-
const previousVote = userVotes[id];
|
|
97
|
-
const isUndoVote = previousVote === type;
|
|
98
|
-
|
|
99
|
-
// Optimistic update
|
|
100
|
-
setUserVotes(prev => {
|
|
101
|
-
const next = { ...prev };
|
|
102
|
-
if (isUndoVote) {
|
|
103
|
-
delete next[id];
|
|
104
|
-
} else {
|
|
105
|
-
next[id] = type;
|
|
106
|
-
}
|
|
107
|
-
return next;
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
setRequests(prev => prev.map(req => {
|
|
111
|
-
if (req.id !== id) return req;
|
|
112
|
-
let delta = 0;
|
|
113
|
-
if (isUndoVote) {
|
|
114
|
-
delta = type === 'up' ? -1 : 1;
|
|
115
|
-
} else if (previousVote) {
|
|
116
|
-
delta = type === 'up' ? 2 : -2;
|
|
117
|
-
} else {
|
|
118
|
-
delta = type === 'up' ? 1 : -1;
|
|
119
|
-
}
|
|
120
|
-
return { ...req, votes: req.votes + delta };
|
|
121
|
-
}));
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
await dataProvider.onVote(id, dataProvider.userId, type);
|
|
125
|
-
} catch (error) {
|
|
126
|
-
// Revert on failure
|
|
127
|
-
setUserVotes(prev => {
|
|
128
|
-
const next = { ...prev };
|
|
129
|
-
if (previousVote) {
|
|
130
|
-
next[id] = previousVote;
|
|
131
|
-
} else {
|
|
132
|
-
delete next[id];
|
|
133
|
-
}
|
|
134
|
-
return next;
|
|
135
|
-
});
|
|
136
|
-
loadData();
|
|
137
|
-
if (__DEV__) console.warn("[FeatureRequestScreen] Vote failed:", error);
|
|
138
|
-
}
|
|
139
|
-
}, [dataProvider, userVotes, loadData]);
|
|
140
|
-
|
|
141
48
|
const handleSubmit = useCallback(async (data: any) => {
|
|
142
|
-
if (!dataProvider?.onSubmit) return;
|
|
143
|
-
|
|
144
49
|
setIsSubmitting(true);
|
|
145
50
|
try {
|
|
146
|
-
await
|
|
51
|
+
await submitRequest({
|
|
147
52
|
title: data.title || "New Request",
|
|
148
53
|
description: data.description,
|
|
149
54
|
type: data.type || "feature_request",
|
|
150
55
|
rating: data.rating,
|
|
151
56
|
});
|
|
152
57
|
setIsModalVisible(false);
|
|
153
|
-
// Reload data to get the new request from the database
|
|
154
|
-
await loadData();
|
|
155
58
|
} catch (error) {
|
|
156
59
|
if (__DEV__) console.warn("[FeatureRequestScreen] Submit failed:", error);
|
|
157
60
|
} finally {
|
|
158
61
|
setIsSubmitting(false);
|
|
159
62
|
}
|
|
160
|
-
}, [
|
|
63
|
+
}, [submitRequest]);
|
|
161
64
|
|
|
162
65
|
const getStatusColor = (status: string) => {
|
|
163
66
|
switch (status) {
|
|
@@ -173,36 +76,28 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
173
76
|
const filteredRequests = (() => {
|
|
174
77
|
switch (activeTab) {
|
|
175
78
|
case 'my':
|
|
176
|
-
return requests.filter(r => r.createdBy ===
|
|
79
|
+
return requests.filter(r => r.createdBy === userId);
|
|
177
80
|
case 'roadmap':
|
|
178
|
-
return requests.filter(r =>
|
|
81
|
+
return requests.filter(r => ['planned', 'completed', 'review'].includes(r.status));
|
|
179
82
|
default:
|
|
180
83
|
return requests;
|
|
181
84
|
}
|
|
182
85
|
})();
|
|
183
86
|
|
|
184
|
-
const
|
|
87
|
+
const renderRequestCard = (item: FeatureRequestItem) => {
|
|
185
88
|
const voted = userVotes[item.id] || null;
|
|
186
89
|
|
|
187
90
|
return (
|
|
188
|
-
<View style={[styles.card, { backgroundColor: tokens.colors.surfaceSecondary, borderColor: tokens.colors.borderLight }]}>
|
|
91
|
+
<View key={item.id} style={[styles.card, { backgroundColor: tokens.colors.surfaceSecondary, borderColor: tokens.colors.borderLight }]}>
|
|
189
92
|
<View style={styles.voteColumn}>
|
|
190
|
-
<TouchableOpacity onPress={() =>
|
|
191
|
-
<AtomicIcon
|
|
192
|
-
name="chevron-up"
|
|
193
|
-
size="md"
|
|
194
|
-
color={voted === 'up' ? "primary" : "textSecondary" as any}
|
|
195
|
-
/>
|
|
93
|
+
<TouchableOpacity onPress={() => vote(item.id, 'up')}>
|
|
94
|
+
<AtomicIcon name="chevron-up" size="md" color={voted === 'up' ? "primary" : "textSecondary" as any} />
|
|
196
95
|
</TouchableOpacity>
|
|
197
96
|
<AtomicText style={[styles.voteCount, { color: voted === 'up' ? tokens.colors.primary : tokens.colors.textPrimary }]}>
|
|
198
97
|
{item.votes}
|
|
199
98
|
</AtomicText>
|
|
200
|
-
<TouchableOpacity onPress={() =>
|
|
201
|
-
<AtomicIcon
|
|
202
|
-
name="chevron-down"
|
|
203
|
-
size="md"
|
|
204
|
-
color={voted === 'down' ? "primary" : "textSecondary" as any}
|
|
205
|
-
/>
|
|
99
|
+
<TouchableOpacity onPress={() => vote(item.id, 'down')}>
|
|
100
|
+
<AtomicIcon name="chevron-down" size="md" color={voted === 'down' ? "primary" : "textSecondary" as any} />
|
|
206
101
|
</TouchableOpacity>
|
|
207
102
|
</View>
|
|
208
103
|
|
|
@@ -221,11 +116,9 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
221
116
|
</AtomicText>
|
|
222
117
|
|
|
223
118
|
<View style={styles.cardFooter}>
|
|
224
|
-
<
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
</AtomicText>
|
|
228
|
-
</View>
|
|
119
|
+
<AtomicText style={[styles.platformText, { color: tokens.colors.textTertiary }]}>
|
|
120
|
+
{item.platform.toUpperCase()}
|
|
121
|
+
</AtomicText>
|
|
229
122
|
<AtomicText style={[styles.commentCount, { color: tokens.colors.textTertiary }]}>
|
|
230
123
|
{item.commentCount} {t.comment_count?.replace('{{count}}', '') || 'comments'}
|
|
231
124
|
</AtomicText>
|
|
@@ -297,13 +190,9 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
297
190
|
</AtomicText>
|
|
298
191
|
</View>
|
|
299
192
|
) : (
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
keyExtractor={item => item.id}
|
|
304
|
-
scrollEnabled={false}
|
|
305
|
-
contentContainerStyle={styles.listContent}
|
|
306
|
-
/>
|
|
193
|
+
<View style={styles.listContent}>
|
|
194
|
+
{filteredRequests.map(renderRequestCard)}
|
|
195
|
+
</View>
|
|
307
196
|
)}
|
|
308
197
|
</ScrollView>
|
|
309
198
|
|
|
@@ -332,180 +221,36 @@ export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ conf
|
|
|
332
221
|
};
|
|
333
222
|
|
|
334
223
|
const styles = StyleSheet.create({
|
|
335
|
-
container: {
|
|
336
|
-
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
},
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
},
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
},
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
},
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
},
|
|
368
|
-
tabLabel: {
|
|
369
|
-
fontSize: 14,
|
|
370
|
-
fontWeight: '600',
|
|
371
|
-
color: 'rgba(255,255,255,0.5)',
|
|
372
|
-
},
|
|
373
|
-
banner: {
|
|
374
|
-
flexDirection: 'row',
|
|
375
|
-
alignItems: 'center',
|
|
376
|
-
padding: 16,
|
|
377
|
-
borderRadius: 16,
|
|
378
|
-
borderWidth: 1,
|
|
379
|
-
gap: 12,
|
|
380
|
-
marginBottom: 20,
|
|
381
|
-
},
|
|
382
|
-
bannerIconContainer: {
|
|
383
|
-
position: 'relative',
|
|
384
|
-
},
|
|
385
|
-
pulseDot: {
|
|
386
|
-
position: 'absolute',
|
|
387
|
-
top: 0,
|
|
388
|
-
right: 0,
|
|
389
|
-
width: 8,
|
|
390
|
-
height: 8,
|
|
391
|
-
borderRadius: 4,
|
|
392
|
-
backgroundColor: '#10b981',
|
|
393
|
-
},
|
|
394
|
-
bannerTitle: {
|
|
395
|
-
fontSize: 14,
|
|
396
|
-
fontWeight: '700',
|
|
397
|
-
},
|
|
398
|
-
bannerSub: {
|
|
399
|
-
fontSize: 12,
|
|
400
|
-
},
|
|
401
|
-
sectionTitle: {
|
|
402
|
-
fontSize: 18,
|
|
403
|
-
fontWeight: '700',
|
|
404
|
-
marginBottom: 16,
|
|
405
|
-
},
|
|
406
|
-
listContent: {
|
|
407
|
-
gap: 12,
|
|
408
|
-
paddingBottom: 40,
|
|
409
|
-
},
|
|
410
|
-
card: {
|
|
411
|
-
flexDirection: 'row',
|
|
412
|
-
padding: 16,
|
|
413
|
-
borderRadius: 16,
|
|
414
|
-
borderWidth: 1,
|
|
415
|
-
gap: 12,
|
|
416
|
-
},
|
|
417
|
-
voteColumn: {
|
|
418
|
-
alignItems: 'center',
|
|
419
|
-
gap: 4,
|
|
420
|
-
width: 40,
|
|
421
|
-
},
|
|
422
|
-
voteCount: {
|
|
423
|
-
fontSize: 13,
|
|
424
|
-
fontWeight: '800',
|
|
425
|
-
},
|
|
426
|
-
cardContent: {
|
|
427
|
-
flex: 1,
|
|
428
|
-
gap: 8,
|
|
429
|
-
},
|
|
430
|
-
cardHeader: {
|
|
431
|
-
flexDirection: 'row',
|
|
432
|
-
justifyContent: 'space-between',
|
|
433
|
-
alignItems: 'flex-start',
|
|
434
|
-
gap: 8,
|
|
435
|
-
},
|
|
436
|
-
cardTitle: {
|
|
437
|
-
fontSize: 15,
|
|
438
|
-
fontWeight: '700',
|
|
439
|
-
flex: 1,
|
|
440
|
-
},
|
|
441
|
-
statusBadge: {
|
|
442
|
-
paddingHorizontal: 8,
|
|
443
|
-
paddingVertical: 2,
|
|
444
|
-
borderRadius: 8,
|
|
445
|
-
borderWidth: 1,
|
|
446
|
-
},
|
|
447
|
-
statusText: {
|
|
448
|
-
fontSize: 9,
|
|
449
|
-
fontWeight: '900',
|
|
450
|
-
},
|
|
451
|
-
cardDescription: {
|
|
452
|
-
fontSize: 13,
|
|
453
|
-
lineHeight: 18,
|
|
454
|
-
},
|
|
455
|
-
cardFooter: {
|
|
456
|
-
flexDirection: 'row',
|
|
457
|
-
alignItems: 'center',
|
|
458
|
-
justifyContent: 'space-between',
|
|
459
|
-
marginTop: 4,
|
|
460
|
-
},
|
|
461
|
-
platformBadge: {
|
|
462
|
-
paddingHorizontal: 6,
|
|
463
|
-
paddingVertical: 2,
|
|
464
|
-
},
|
|
465
|
-
platformText: {
|
|
466
|
-
fontSize: 10,
|
|
467
|
-
fontWeight: '600',
|
|
468
|
-
textTransform: 'uppercase',
|
|
469
|
-
},
|
|
470
|
-
commentCount: {
|
|
471
|
-
fontSize: 11,
|
|
472
|
-
},
|
|
473
|
-
floatingHint: {
|
|
474
|
-
position: 'absolute',
|
|
475
|
-
bottom: 40,
|
|
476
|
-
right: 16,
|
|
477
|
-
zIndex: 100,
|
|
478
|
-
},
|
|
479
|
-
hintBadge: {
|
|
480
|
-
paddingHorizontal: 12,
|
|
481
|
-
paddingVertical: 6,
|
|
482
|
-
borderRadius: 20,
|
|
483
|
-
shadowColor: '#000',
|
|
484
|
-
shadowOffset: { width: 0, height: 4 },
|
|
485
|
-
shadowOpacity: 0.3,
|
|
486
|
-
shadowRadius: 4,
|
|
487
|
-
elevation: 8,
|
|
488
|
-
},
|
|
489
|
-
hintText: {
|
|
490
|
-
color: '#fff',
|
|
491
|
-
fontSize: 10,
|
|
492
|
-
fontWeight: '900',
|
|
493
|
-
textTransform: 'uppercase',
|
|
494
|
-
},
|
|
495
|
-
loadingContainer: {
|
|
496
|
-
flex: 1,
|
|
497
|
-
justifyContent: 'center',
|
|
498
|
-
alignItems: 'center',
|
|
499
|
-
paddingTop: 100,
|
|
500
|
-
},
|
|
501
|
-
emptyState: {
|
|
502
|
-
alignItems: 'center',
|
|
503
|
-
justifyContent: 'center',
|
|
504
|
-
paddingVertical: 60,
|
|
505
|
-
gap: 12,
|
|
506
|
-
},
|
|
507
|
-
emptyText: {
|
|
508
|
-
fontSize: 14,
|
|
509
|
-
fontWeight: '500',
|
|
510
|
-
},
|
|
224
|
+
container: { padding: 16 },
|
|
225
|
+
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12 },
|
|
226
|
+
headerTitle: { fontSize: 20, fontWeight: '800' },
|
|
227
|
+
addButton: { width: 36, height: 36, borderRadius: 12, alignItems: 'center', justifyContent: 'center' },
|
|
228
|
+
tabsContainer: { flexDirection: 'row', paddingHorizontal: 16, borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.05)' },
|
|
229
|
+
tab: { paddingVertical: 12, paddingHorizontal: 16, borderBottomWidth: 2, borderBottomColor: 'transparent' },
|
|
230
|
+
tabLabel: { fontSize: 14, fontWeight: '600', color: 'rgba(255,255,255,0.5)' },
|
|
231
|
+
banner: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, borderWidth: 1, gap: 12, marginBottom: 20 },
|
|
232
|
+
bannerIconContainer: { position: 'relative' },
|
|
233
|
+
pulseDot: { position: 'absolute', top: 0, right: 0, width: 8, height: 8, borderRadius: 4, backgroundColor: '#10b981' },
|
|
234
|
+
bannerTitle: { fontSize: 14, fontWeight: '700' },
|
|
235
|
+
bannerSub: { fontSize: 12 },
|
|
236
|
+
sectionTitle: { fontSize: 18, fontWeight: '700', marginBottom: 16 },
|
|
237
|
+
listContent: { gap: 12, paddingBottom: 40 },
|
|
238
|
+
card: { flexDirection: 'row', padding: 16, borderRadius: 16, borderWidth: 1, gap: 12 },
|
|
239
|
+
voteColumn: { alignItems: 'center', gap: 4, width: 40 },
|
|
240
|
+
voteCount: { fontSize: 13, fontWeight: '800' },
|
|
241
|
+
cardContent: { flex: 1, gap: 8 },
|
|
242
|
+
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', gap: 8 },
|
|
243
|
+
cardTitle: { fontSize: 15, fontWeight: '700', flex: 1 },
|
|
244
|
+
statusBadge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 8, borderWidth: 1 },
|
|
245
|
+
statusText: { fontSize: 9, fontWeight: '900' },
|
|
246
|
+
cardDescription: { fontSize: 13, lineHeight: 18 },
|
|
247
|
+
cardFooter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 4 },
|
|
248
|
+
platformText: { fontSize: 10, fontWeight: '600' },
|
|
249
|
+
commentCount: { fontSize: 11 },
|
|
250
|
+
floatingHint: { position: 'absolute', bottom: 40, right: 16, zIndex: 100 },
|
|
251
|
+
hintBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 4, elevation: 8 },
|
|
252
|
+
hintText: { color: '#fff', fontSize: 10, fontWeight: '900', textTransform: 'uppercase' },
|
|
253
|
+
loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingTop: 100 },
|
|
254
|
+
emptyState: { alignItems: 'center', justifyContent: 'center', paddingVertical: 60, gap: 12 },
|
|
255
|
+
emptyText: { fontSize: 14, fontWeight: '500' },
|
|
511
256
|
});
|
|
@@ -14,7 +14,6 @@ import { translateFAQData } from "../utils/faqTranslator";
|
|
|
14
14
|
import { useSettingsConfigFactory } from "../utils/settingsConfigFactory";
|
|
15
15
|
import type { SettingsConfig, SettingsTranslations } from "../screens/types";
|
|
16
16
|
import type { FeedbackFormData } from "../utils/config-creators";
|
|
17
|
-
import type { FeatureRequestDataProvider } from "../../domains/feedback/domain/entities/FeatureRequestEntity";
|
|
18
17
|
import type { AppInfo, FAQData, UserProfileDisplay, AdditionalScreen, AccountConfig } from "../navigation/types";
|
|
19
18
|
|
|
20
19
|
export interface SettingsFeatures {
|
|
@@ -36,7 +35,6 @@ export interface UseSettingsScreenConfigParams {
|
|
|
36
35
|
faqData?: FAQData;
|
|
37
36
|
isPremium: boolean;
|
|
38
37
|
onFeedbackSubmit: (data: FeedbackFormData) => Promise<void>;
|
|
39
|
-
featureRequestDataProvider?: FeatureRequestDataProvider;
|
|
40
38
|
additionalScreens?: AdditionalScreen[];
|
|
41
39
|
features?: SettingsFeatures;
|
|
42
40
|
translations?: SettingsTranslations;
|
|
@@ -54,7 +52,7 @@ export interface SettingsScreenConfigResult {
|
|
|
54
52
|
export const useSettingsScreenConfig = (
|
|
55
53
|
params: UseSettingsScreenConfigParams
|
|
56
54
|
): SettingsScreenConfigResult => {
|
|
57
|
-
const { appInfo, faqData, isPremium, onFeedbackSubmit,
|
|
55
|
+
const { appInfo, faqData, isPremium, onFeedbackSubmit, features = {}, translations } = params;
|
|
58
56
|
|
|
59
57
|
const {
|
|
60
58
|
notifications: showNotifications = true,
|
|
@@ -104,15 +102,6 @@ export const useSettingsScreenConfig = (
|
|
|
104
102
|
translations,
|
|
105
103
|
};
|
|
106
104
|
|
|
107
|
-
// Merge dataProvider into feedback config
|
|
108
|
-
if (featureRequestDataProvider && config.feedback) {
|
|
109
|
-
const existingFeedback = typeof config.feedback === 'object' ? config.feedback : { enabled: true };
|
|
110
|
-
config.feedback = {
|
|
111
|
-
...existingFeedback,
|
|
112
|
-
dataProvider: featureRequestDataProvider,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
105
|
if (config.subscription && typeof config.subscription === 'object') {
|
|
117
106
|
config.subscription = {
|
|
118
107
|
...config.subscription,
|
|
@@ -143,7 +132,7 @@ export const useSettingsScreenConfig = (
|
|
|
143
132
|
}
|
|
144
133
|
|
|
145
134
|
return config;
|
|
146
|
-
}, [baseSettingsConfig, translations
|
|
135
|
+
}, [baseSettingsConfig, translations]);
|
|
147
136
|
|
|
148
137
|
const userProfile = useMemo(() => createUserProfileDisplay({
|
|
149
138
|
profileData: userProfileData,
|
|
@@ -133,12 +133,9 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
|
|
|
133
133
|
title: videoTutorialConfig?.title || featureTranslations?.videoTutorial?.title || "",
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
const feedbackConfig = typeof config?.feedback === 'object' ? config.feedback : undefined;
|
|
137
136
|
const featureRequestScreen = createScreenWithProps("FeatureRequest", FeatureRequestScreen, {
|
|
138
137
|
config: {
|
|
139
|
-
...feedbackConfig,
|
|
140
138
|
translations: translations?.feedback,
|
|
141
|
-
dataProvider: feedbackConfig?.dataProvider,
|
|
142
139
|
},
|
|
143
140
|
texts: {
|
|
144
141
|
feedbackTypes: [
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import type { FeatureVisibility } from "./BaseTypes";
|
|
7
7
|
import type { FeedbackType } from "../../../domains/feedback/domain/entities/FeedbackEntity";
|
|
8
|
-
import type { FeatureRequestDataProvider } from "../../../domains/feedback/domain/entities/FeatureRequestEntity";
|
|
9
8
|
import type { FAQCategory } from "../../../domains/faqs/domain/entities/FAQEntity";
|
|
10
9
|
import type { SettingsStackParamList } from "../../navigation/types";
|
|
11
10
|
|
|
@@ -45,8 +44,6 @@ export interface FeedbackConfig {
|
|
|
45
44
|
onSubmit?: (data: { type: FeedbackType; rating: number; description: string; title: string }) => Promise<void>;
|
|
46
45
|
/** Custom handler to open feedback screen (overrides default modal) */
|
|
47
46
|
onPress?: () => void;
|
|
48
|
-
/** Data provider for feature request screen (fetching, voting, submitting) */
|
|
49
|
-
dataProvider?: FeatureRequestDataProvider;
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
/**
|