@umituz/react-native-settings 5.3.39 → 5.3.41
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 +42 -0
- package/dist/domains/feedback/index.d.ts +2 -1
- package/dist/domains/feedback/presentation/screens/FeatureRequestScreen.d.ts +11 -1
- package/dist/presentation/hooks/useSettingsScreenConfig.d.ts +2 -0
- package/dist/presentation/screens/types/SettingsTranslations.d.ts +25 -0
- package/dist/presentation/screens/types/UserFeatureConfig.d.ts +3 -0
- package/package.json +1 -1
- package/src/domains/feedback/domain/entities/FeatureRequestEntity.ts +48 -0
- package/src/domains/feedback/index.ts +2 -1
- package/src/domains/feedback/presentation/screens/FeatureRequestScreen.tsx +241 -180
- package/src/presentation/hooks/useSettingsScreenConfig.ts +13 -2
- package/src/presentation/navigation/hooks/useSettingsScreens.ts +17 -1
- package/src/presentation/screens/types/SettingsTranslations.ts +25 -0
- package/src/presentation/screens/types/UserFeatureConfig.ts +3 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Request Entity
|
|
3
|
+
* Represents a community feature request that all users can see and vote on
|
|
4
|
+
*/
|
|
5
|
+
export type FeatureRequestStatus = 'pending' | 'planned' | 'review' | 'completed' | 'dismissed';
|
|
6
|
+
export type VoteType = 'up' | 'down';
|
|
7
|
+
export interface FeatureRequestItem {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
type: string;
|
|
12
|
+
status: FeatureRequestStatus;
|
|
13
|
+
votes: number;
|
|
14
|
+
commentCount: number;
|
|
15
|
+
createdBy: string;
|
|
16
|
+
isAnonymous: boolean;
|
|
17
|
+
platform: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
updatedAt: string;
|
|
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
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Feedback Domain
|
|
3
|
-
* User feedback, bug reports
|
|
3
|
+
* User feedback, bug reports, feature requests
|
|
4
4
|
*/
|
|
5
5
|
export * from './presentation/components/FeedbackForm';
|
|
6
6
|
export * from './presentation/components/FeedbackModal';
|
|
7
7
|
export * from './presentation/components/SupportSection';
|
|
8
8
|
export * from './presentation/hooks/useFeedbackForm';
|
|
9
9
|
export * from './domain/entities/FeedbackEntity';
|
|
10
|
+
export * from './domain/entities/FeatureRequestEntity';
|
|
10
11
|
export * from './domain/repositories/IFeedbackRepository';
|
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
|
|
2
|
+
import type { FeatureRequestDataProvider } from "../../domain/entities/FeatureRequestEntity";
|
|
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 {};
|
|
@@ -7,6 +7,7 @@
|
|
|
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";
|
|
10
11
|
import type { AppInfo, FAQData, UserProfileDisplay, AdditionalScreen, AccountConfig } from "../navigation/types";
|
|
11
12
|
export interface SettingsFeatures {
|
|
12
13
|
notifications?: boolean;
|
|
@@ -26,6 +27,7 @@ export interface UseSettingsScreenConfigParams {
|
|
|
26
27
|
faqData?: FAQData;
|
|
27
28
|
isPremium: boolean;
|
|
28
29
|
onFeedbackSubmit: (data: FeedbackFormData) => Promise<void>;
|
|
30
|
+
featureRequestDataProvider?: FeatureRequestDataProvider;
|
|
29
31
|
additionalScreens?: AdditionalScreen[];
|
|
30
32
|
features?: SettingsFeatures;
|
|
31
33
|
translations?: SettingsTranslations;
|
|
@@ -128,6 +128,31 @@ export interface SettingsTranslations {
|
|
|
128
128
|
description?: string;
|
|
129
129
|
};
|
|
130
130
|
};
|
|
131
|
+
/** Feature Request Screen translations */
|
|
132
|
+
feedback?: {
|
|
133
|
+
screen_title?: string;
|
|
134
|
+
tabs?: {
|
|
135
|
+
all?: string;
|
|
136
|
+
my?: string;
|
|
137
|
+
roadmap?: string;
|
|
138
|
+
};
|
|
139
|
+
banner?: {
|
|
140
|
+
title?: string;
|
|
141
|
+
subtitle?: string;
|
|
142
|
+
};
|
|
143
|
+
trending?: string;
|
|
144
|
+
voter_count?: string;
|
|
145
|
+
comment_count?: string;
|
|
146
|
+
new_idea?: string;
|
|
147
|
+
empty?: string;
|
|
148
|
+
status?: {
|
|
149
|
+
planned?: string;
|
|
150
|
+
review?: string;
|
|
151
|
+
completed?: string;
|
|
152
|
+
pending?: string;
|
|
153
|
+
dismissed?: string;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
131
156
|
feedbackModal?: {
|
|
132
157
|
title?: string;
|
|
133
158
|
ratingLabel?: string;
|
|
@@ -4,6 +4,7 @@
|
|
|
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";
|
|
7
8
|
import type { FAQCategory } from "../../../domains/faqs/domain/entities/FAQEntity";
|
|
8
9
|
import type { SettingsStackParamList } from "../../navigation/types";
|
|
9
10
|
/**
|
|
@@ -46,6 +47,8 @@ export interface FeedbackConfig {
|
|
|
46
47
|
}) => Promise<void>;
|
|
47
48
|
/** Custom handler to open feedback screen (overrides default modal) */
|
|
48
49
|
onPress?: () => void;
|
|
50
|
+
/** Data provider for feature request screen (fetching, voting, submitting) */
|
|
51
|
+
dataProvider?: FeatureRequestDataProvider;
|
|
49
52
|
}
|
|
50
53
|
/**
|
|
51
54
|
* 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.41",
|
|
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",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Request Entity
|
|
3
|
+
* Represents a community feature request that all users can see and vote on
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type FeatureRequestStatus = 'pending' | 'planned' | 'review' | 'completed' | 'dismissed';
|
|
7
|
+
|
|
8
|
+
export type VoteType = 'up' | 'down';
|
|
9
|
+
|
|
10
|
+
export interface FeatureRequestItem {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
type: string;
|
|
15
|
+
status: FeatureRequestStatus;
|
|
16
|
+
votes: number;
|
|
17
|
+
commentCount: number;
|
|
18
|
+
createdBy: string;
|
|
19
|
+
isAnonymous: boolean;
|
|
20
|
+
platform: string;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt: string;
|
|
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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Feedback Domain
|
|
3
|
-
* User feedback, bug reports
|
|
3
|
+
* User feedback, bug reports, feature requests
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export * from './presentation/components/FeedbackForm';
|
|
@@ -8,4 +8,5 @@ export * from './presentation/components/FeedbackModal';
|
|
|
8
8
|
export * from './presentation/components/SupportSection';
|
|
9
9
|
export * from './presentation/hooks/useFeedbackForm';
|
|
10
10
|
export * from './domain/entities/FeedbackEntity';
|
|
11
|
+
export * from './domain/entities/FeatureRequestEntity';
|
|
11
12
|
export * from './domain/repositories/IFeedbackRepository';
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import React, { useState,
|
|
1
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
StyleSheet,
|
|
5
5
|
TouchableOpacity,
|
|
6
6
|
ScrollView,
|
|
7
7
|
FlatList,
|
|
8
|
-
|
|
8
|
+
ActivityIndicator,
|
|
9
9
|
} from "react-native";
|
|
10
10
|
import {
|
|
11
11
|
AtomicText,
|
|
@@ -15,177 +15,230 @@ import {
|
|
|
15
15
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
16
16
|
import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
|
|
17
17
|
import { FeedbackModal } from "../components/FeedbackModal";
|
|
18
|
-
import type { FeedbackType
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
votes: number;
|
|
25
|
-
status: 'planned' | 'review' | 'completed';
|
|
26
|
-
comments: number;
|
|
27
|
-
userAvatars: string[];
|
|
28
|
-
voted: 'up' | 'down' | null;
|
|
29
|
-
}
|
|
18
|
+
import type { FeedbackType } from "../../domain/entities/FeedbackEntity";
|
|
19
|
+
import type {
|
|
20
|
+
FeatureRequestItem,
|
|
21
|
+
FeatureRequestDataProvider,
|
|
22
|
+
VoteType,
|
|
23
|
+
} from "../../domain/entities/FeatureRequestEntity";
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
userAvatars: ["https://i.pravatar.cc/100?u=1", "https://i.pravatar.cc/100?u=2"],
|
|
40
|
-
voted: null,
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
id: "2",
|
|
44
|
-
title: "Couple Video Templates",
|
|
45
|
-
description: "Specific cinematic transition templates designed for couples' vlogs and anniversary montages.",
|
|
46
|
-
votes: 842,
|
|
47
|
-
status: 'review',
|
|
48
|
-
comments: 8,
|
|
49
|
-
userAvatars: ["https://i.pravatar.cc/100?u=3"],
|
|
50
|
-
voted: null,
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
id: "3",
|
|
54
|
-
title: "4K Export Support",
|
|
55
|
-
description: "Allow users to export generated AI videos in high-quality 4K resolution.",
|
|
56
|
-
votes: 2100,
|
|
57
|
-
status: 'completed',
|
|
58
|
-
comments: 42,
|
|
59
|
-
userAvatars: ["https://i.pravatar.cc/100?u=4", "https://i.pravatar.cc/100?u=5"],
|
|
60
|
-
voted: null,
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: "4",
|
|
64
|
-
title: "Dark Mode Editor",
|
|
65
|
-
description: "Make the entire editing interface dark for better focus during late-night creative sessions.",
|
|
66
|
-
votes: 156,
|
|
67
|
-
status: 'planned',
|
|
68
|
-
comments: 5,
|
|
69
|
-
userAvatars: ["https://i.pravatar.cc/100?u=6"],
|
|
70
|
-
voted: null,
|
|
71
|
-
}
|
|
72
|
-
];
|
|
25
|
+
interface FeatureRequestScreenProps {
|
|
26
|
+
config?: {
|
|
27
|
+
translations?: Record<string, any>;
|
|
28
|
+
dataProvider?: FeatureRequestDataProvider;
|
|
29
|
+
onSubmit?: (data: any) => Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
texts: any;
|
|
32
|
+
}
|
|
73
33
|
|
|
74
|
-
export const FeatureRequestScreen: React.FC<
|
|
34
|
+
export const FeatureRequestScreen: React.FC<FeatureRequestScreenProps> = ({ config, texts }) => {
|
|
75
35
|
const tokens = useAppDesignTokens();
|
|
76
|
-
const
|
|
36
|
+
const dataProvider = config?.dataProvider;
|
|
37
|
+
|
|
38
|
+
const [requests, setRequests] = useState<FeatureRequestItem[]>([]);
|
|
39
|
+
const [userVotes, setUserVotes] = useState<Record<string, VoteType>>({});
|
|
40
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
77
41
|
const [activeTab, setActiveTab] = useState<'all' | 'my' | 'roadmap'>('all');
|
|
78
42
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
43
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
79
44
|
|
|
80
|
-
// Use passed translations if available
|
|
81
45
|
const t = config?.translations || {};
|
|
82
46
|
const screenTitle = t.screen_title || "Feedback & Features";
|
|
83
47
|
const trendingTitle = t.trending || "Trending Requests";
|
|
84
48
|
const bannerTitle = t.banner?.title || "Community Active";
|
|
85
|
-
const bannerSub = t.banner?.subtitle ||
|
|
49
|
+
const bannerSub = t.banner?.subtitle || `${requests.length} feature requests`;
|
|
86
50
|
const newIdeaLabel = t.new_idea || "NEW IDEA?";
|
|
87
|
-
|
|
51
|
+
|
|
88
52
|
const tabLabels = {
|
|
89
53
|
all: t.tabs?.all || "All Requests",
|
|
90
54
|
my: t.tabs?.my || "My Feedback",
|
|
91
55
|
roadmap: t.tabs?.roadmap || "Roadmap",
|
|
92
56
|
};
|
|
93
57
|
|
|
94
|
-
const statusLabels = {
|
|
58
|
+
const statusLabels: Record<string, string> = {
|
|
95
59
|
planned: t.status?.planned || "Planned",
|
|
96
60
|
review: t.status?.review || "Under Review",
|
|
97
61
|
completed: t.status?.completed || "Completed",
|
|
62
|
+
pending: t.status?.pending || "Pending",
|
|
63
|
+
dismissed: t.status?.dismissed || "Dismissed",
|
|
98
64
|
};
|
|
99
65
|
|
|
100
|
-
const
|
|
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
|
+
|
|
101
110
|
setRequests(prev => prev.map(req => {
|
|
102
|
-
if (req.id
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
newVotes += type === 'up' ? 2 : -2;
|
|
111
|
-
} else {
|
|
112
|
-
newVotes += type === 'up' ? 1 : -1;
|
|
113
|
-
}
|
|
114
|
-
return { ...req, votes: newVotes, voted: type };
|
|
115
|
-
}
|
|
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;
|
|
116
119
|
}
|
|
117
|
-
return req;
|
|
120
|
+
return { ...req, votes: req.votes + delta };
|
|
118
121
|
}));
|
|
119
|
-
};
|
|
120
122
|
|
|
121
|
-
|
|
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
|
+
const handleSubmit = useCallback(async (data: any) => {
|
|
142
|
+
if (!dataProvider?.onSubmit) return;
|
|
143
|
+
|
|
144
|
+
setIsSubmitting(true);
|
|
145
|
+
try {
|
|
146
|
+
await dataProvider.onSubmit({
|
|
147
|
+
title: data.title || "New Request",
|
|
148
|
+
description: data.description,
|
|
149
|
+
type: data.type || "feature_request",
|
|
150
|
+
rating: data.rating,
|
|
151
|
+
});
|
|
152
|
+
setIsModalVisible(false);
|
|
153
|
+
// Reload data to get the new request from the database
|
|
154
|
+
await loadData();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (__DEV__) console.warn("[FeatureRequestScreen] Submit failed:", error);
|
|
157
|
+
} finally {
|
|
158
|
+
setIsSubmitting(false);
|
|
159
|
+
}
|
|
160
|
+
}, [dataProvider, loadData]);
|
|
161
|
+
|
|
162
|
+
const getStatusColor = (status: string) => {
|
|
122
163
|
switch (status) {
|
|
123
164
|
case 'planned': return '#3b82f6';
|
|
124
165
|
case 'review': return '#f59e0b';
|
|
125
166
|
case 'completed': return '#10b981';
|
|
167
|
+
case 'pending': return '#8b5cf6';
|
|
168
|
+
case 'dismissed': return '#ef4444';
|
|
126
169
|
default: return tokens.colors.textSecondary;
|
|
127
170
|
}
|
|
128
171
|
};
|
|
129
172
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<AtomicText style={[styles.voteCount, { color: item.voted === 'up' ? tokens.colors.primary : tokens.colors.textPrimary }]}>
|
|
141
|
-
{item.votes}
|
|
142
|
-
</AtomicText>
|
|
143
|
-
<TouchableOpacity onPress={() => handleVote(item.id, 'down')}>
|
|
144
|
-
<AtomicIcon
|
|
145
|
-
name="chevron-down"
|
|
146
|
-
size="md"
|
|
147
|
-
color={item.voted === 'down' ? "primary" : "textSecondary" as any}
|
|
148
|
-
/>
|
|
149
|
-
</TouchableOpacity>
|
|
150
|
-
</View>
|
|
173
|
+
const filteredRequests = (() => {
|
|
174
|
+
switch (activeTab) {
|
|
175
|
+
case 'my':
|
|
176
|
+
return requests.filter(r => r.createdBy === dataProvider?.userId);
|
|
177
|
+
case 'roadmap':
|
|
178
|
+
return requests.filter(r => r.status === 'planned' || r.status === 'completed' || r.status === 'review');
|
|
179
|
+
default:
|
|
180
|
+
return requests;
|
|
181
|
+
}
|
|
182
|
+
})();
|
|
151
183
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
184
|
+
const renderRequestItem = ({ item }: { item: FeatureRequestItem }) => {
|
|
185
|
+
const voted = userVotes[item.id] || null;
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<View style={[styles.card, { backgroundColor: tokens.colors.surfaceSecondary, borderColor: tokens.colors.borderLight }]}>
|
|
189
|
+
<View style={styles.voteColumn}>
|
|
190
|
+
<TouchableOpacity onPress={() => handleVote(item.id, 'up')}>
|
|
191
|
+
<AtomicIcon
|
|
192
|
+
name="chevron-up"
|
|
193
|
+
size="md"
|
|
194
|
+
color={voted === 'up' ? "primary" : "textSecondary" as any}
|
|
195
|
+
/>
|
|
196
|
+
</TouchableOpacity>
|
|
197
|
+
<AtomicText style={[styles.voteCount, { color: voted === 'up' ? tokens.colors.primary : tokens.colors.textPrimary }]}>
|
|
198
|
+
{item.votes}
|
|
199
|
+
</AtomicText>
|
|
200
|
+
<TouchableOpacity onPress={() => handleVote(item.id, 'down')}>
|
|
201
|
+
<AtomicIcon
|
|
202
|
+
name="chevron-down"
|
|
203
|
+
size="md"
|
|
204
|
+
color={voted === 'down' ? "primary" : "textSecondary" as any}
|
|
205
|
+
/>
|
|
206
|
+
</TouchableOpacity>
|
|
160
207
|
</View>
|
|
161
|
-
|
|
162
|
-
<
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
))}
|
|
171
|
-
{item.comments > 5 && (
|
|
172
|
-
<View style={[styles.avatarMore, { backgroundColor: tokens.colors.surfaceVariant }]}>
|
|
173
|
-
<AtomicText style={styles.avatarMoreText}>+{item.comments}</AtomicText>
|
|
174
|
-
</View>
|
|
175
|
-
)}
|
|
208
|
+
|
|
209
|
+
<View style={styles.cardContent}>
|
|
210
|
+
<View style={styles.cardHeader}>
|
|
211
|
+
<AtomicText style={styles.cardTitle}>{item.title}</AtomicText>
|
|
212
|
+
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) + '20', borderColor: getStatusColor(item.status) + '40' }]}>
|
|
213
|
+
<AtomicText style={[styles.statusText, { color: getStatusColor(item.status) }]}>
|
|
214
|
+
{(statusLabels[item.status] || item.status).toUpperCase()}
|
|
215
|
+
</AtomicText>
|
|
216
|
+
</View>
|
|
176
217
|
</View>
|
|
177
|
-
|
|
178
|
-
|
|
218
|
+
|
|
219
|
+
<AtomicText style={[styles.cardDescription, { color: tokens.colors.textSecondary }]}>
|
|
220
|
+
{item.description}
|
|
179
221
|
</AtomicText>
|
|
222
|
+
|
|
223
|
+
<View style={styles.cardFooter}>
|
|
224
|
+
<View style={styles.platformBadge}>
|
|
225
|
+
<AtomicText style={[styles.platformText, { color: tokens.colors.textTertiary }]}>
|
|
226
|
+
{item.platform}
|
|
227
|
+
</AtomicText>
|
|
228
|
+
</View>
|
|
229
|
+
<AtomicText style={[styles.commentCount, { color: tokens.colors.textTertiary }]}>
|
|
230
|
+
{item.commentCount} {t.comment_count?.replace('{{count}}', '') || 'comments'}
|
|
231
|
+
</AtomicText>
|
|
232
|
+
</View>
|
|
180
233
|
</View>
|
|
181
234
|
</View>
|
|
182
|
-
|
|
183
|
-
|
|
235
|
+
);
|
|
236
|
+
};
|
|
184
237
|
|
|
185
238
|
const header = (
|
|
186
239
|
<View style={styles.header}>
|
|
187
240
|
<AtomicText style={styles.headerTitle}>{screenTitle}</AtomicText>
|
|
188
|
-
<TouchableOpacity
|
|
241
|
+
<TouchableOpacity
|
|
189
242
|
style={[styles.addButton, { backgroundColor: tokens.colors.primary }]}
|
|
190
243
|
onPress={() => setIsModalVisible(true)}
|
|
191
244
|
>
|
|
@@ -194,11 +247,21 @@ export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
|
|
|
194
247
|
</View>
|
|
195
248
|
);
|
|
196
249
|
|
|
250
|
+
if (isLoading) {
|
|
251
|
+
return (
|
|
252
|
+
<ScreenLayout header={header} edges={['top', 'bottom']}>
|
|
253
|
+
<View style={styles.loadingContainer}>
|
|
254
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
255
|
+
</View>
|
|
256
|
+
</ScreenLayout>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
197
260
|
return (
|
|
198
261
|
<ScreenLayout header={header} edges={['top', 'bottom']}>
|
|
199
262
|
<View style={styles.tabsContainer}>
|
|
200
263
|
{(['all', 'my', 'roadmap'] as const).map((tab) => (
|
|
201
|
-
<TouchableOpacity
|
|
264
|
+
<TouchableOpacity
|
|
202
265
|
key={tab}
|
|
203
266
|
onPress={() => setActiveTab(tab)}
|
|
204
267
|
style={[styles.tab, activeTab === tab && { borderBottomColor: tokens.colors.primary }]}
|
|
@@ -226,45 +289,43 @@ export const FeatureRequestScreen: React.FC<any> = ({ config, texts }) => {
|
|
|
226
289
|
|
|
227
290
|
<AtomicText style={styles.sectionTitle}>{trendingTitle}</AtomicText>
|
|
228
291
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
292
|
+
{filteredRequests.length === 0 ? (
|
|
293
|
+
<View style={styles.emptyState}>
|
|
294
|
+
<AtomicIcon name="chatbubble-outline" size="xl" color="textTertiary" />
|
|
295
|
+
<AtomicText style={[styles.emptyText, { color: tokens.colors.textTertiary }]}>
|
|
296
|
+
{t.empty || "No requests yet. Be the first!"}
|
|
297
|
+
</AtomicText>
|
|
298
|
+
</View>
|
|
299
|
+
) : (
|
|
300
|
+
<FlatList
|
|
301
|
+
data={filteredRequests}
|
|
302
|
+
renderItem={renderRequestItem}
|
|
303
|
+
keyExtractor={item => item.id}
|
|
304
|
+
scrollEnabled={false}
|
|
305
|
+
contentContainerStyle={styles.listContent}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
236
308
|
</ScrollView>
|
|
237
309
|
|
|
238
310
|
{isModalVisible && (
|
|
239
311
|
<FeedbackModal
|
|
240
312
|
visible={isModalVisible}
|
|
241
313
|
onClose={() => setIsModalVisible(false)}
|
|
242
|
-
title={texts
|
|
243
|
-
onSubmit={
|
|
244
|
-
console.log("Submitted:", data);
|
|
245
|
-
setIsModalVisible(false);
|
|
246
|
-
// Add to mock list for "live" feel
|
|
247
|
-
const newReq: FeatureRequest = {
|
|
248
|
-
id: Date.now().toString(),
|
|
249
|
-
title: data.title || "New Request",
|
|
250
|
-
description: data.description,
|
|
251
|
-
votes: 1,
|
|
252
|
-
status: 'review',
|
|
253
|
-
comments: 0,
|
|
254
|
-
userAvatars: ["https://i.pravatar.cc/100?u=me"],
|
|
255
|
-
voted: 'up',
|
|
256
|
-
};
|
|
257
|
-
setRequests(prev => [newReq, ...prev]);
|
|
258
|
-
}}
|
|
314
|
+
title={texts?.title}
|
|
315
|
+
onSubmit={handleSubmit}
|
|
259
316
|
texts={texts}
|
|
260
317
|
initialType="feature_request"
|
|
318
|
+
isSubmitting={isSubmitting}
|
|
261
319
|
/>
|
|
262
320
|
)}
|
|
263
321
|
|
|
264
322
|
<View style={styles.floatingHint}>
|
|
265
|
-
<
|
|
323
|
+
<TouchableOpacity
|
|
324
|
+
style={[styles.hintBadge, { backgroundColor: tokens.colors.primary }]}
|
|
325
|
+
onPress={() => setIsModalVisible(true)}
|
|
326
|
+
>
|
|
266
327
|
<AtomicText style={styles.hintText}>{newIdeaLabel}</AtomicText>
|
|
267
|
-
</
|
|
328
|
+
</TouchableOpacity>
|
|
268
329
|
</View>
|
|
269
330
|
</ScreenLayout>
|
|
270
331
|
);
|
|
@@ -397,30 +458,14 @@ const styles = StyleSheet.create({
|
|
|
397
458
|
justifyContent: 'space-between',
|
|
398
459
|
marginTop: 4,
|
|
399
460
|
},
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
},
|
|
404
|
-
avatar: {
|
|
405
|
-
width: 24,
|
|
406
|
-
height: 24,
|
|
407
|
-
borderRadius: 12,
|
|
408
|
-
borderWidth: 2,
|
|
409
|
-
marginLeft: -8,
|
|
410
|
-
},
|
|
411
|
-
avatarMore: {
|
|
412
|
-
width: 24,
|
|
413
|
-
height: 24,
|
|
414
|
-
borderRadius: 12,
|
|
415
|
-
alignItems: 'center',
|
|
416
|
-
justifyContent: 'center',
|
|
417
|
-
marginLeft: -8,
|
|
418
|
-
borderWidth: 2,
|
|
419
|
-
borderColor: 'transparent',
|
|
461
|
+
platformBadge: {
|
|
462
|
+
paddingHorizontal: 6,
|
|
463
|
+
paddingVertical: 2,
|
|
420
464
|
},
|
|
421
|
-
|
|
422
|
-
fontSize:
|
|
423
|
-
fontWeight: '
|
|
465
|
+
platformText: {
|
|
466
|
+
fontSize: 10,
|
|
467
|
+
fontWeight: '600',
|
|
468
|
+
textTransform: 'uppercase',
|
|
424
469
|
},
|
|
425
470
|
commentCount: {
|
|
426
471
|
fontSize: 11,
|
|
@@ -446,5 +491,21 @@ const styles = StyleSheet.create({
|
|
|
446
491
|
fontSize: 10,
|
|
447
492
|
fontWeight: '900',
|
|
448
493
|
textTransform: 'uppercase',
|
|
449
|
-
}
|
|
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
|
+
},
|
|
450
511
|
});
|
|
@@ -14,6 +14,7 @@ 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";
|
|
17
18
|
import type { AppInfo, FAQData, UserProfileDisplay, AdditionalScreen, AccountConfig } from "../navigation/types";
|
|
18
19
|
|
|
19
20
|
export interface SettingsFeatures {
|
|
@@ -35,6 +36,7 @@ export interface UseSettingsScreenConfigParams {
|
|
|
35
36
|
faqData?: FAQData;
|
|
36
37
|
isPremium: boolean;
|
|
37
38
|
onFeedbackSubmit: (data: FeedbackFormData) => Promise<void>;
|
|
39
|
+
featureRequestDataProvider?: FeatureRequestDataProvider;
|
|
38
40
|
additionalScreens?: AdditionalScreen[];
|
|
39
41
|
features?: SettingsFeatures;
|
|
40
42
|
translations?: SettingsTranslations;
|
|
@@ -52,7 +54,7 @@ export interface SettingsScreenConfigResult {
|
|
|
52
54
|
export const useSettingsScreenConfig = (
|
|
53
55
|
params: UseSettingsScreenConfigParams
|
|
54
56
|
): SettingsScreenConfigResult => {
|
|
55
|
-
const { appInfo, faqData, isPremium, onFeedbackSubmit, features = {}, translations } = params;
|
|
57
|
+
const { appInfo, faqData, isPremium, onFeedbackSubmit, featureRequestDataProvider, features = {}, translations } = params;
|
|
56
58
|
|
|
57
59
|
const {
|
|
58
60
|
notifications: showNotifications = true,
|
|
@@ -102,6 +104,15 @@ export const useSettingsScreenConfig = (
|
|
|
102
104
|
translations,
|
|
103
105
|
};
|
|
104
106
|
|
|
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
|
+
|
|
105
116
|
if (config.subscription && typeof config.subscription === 'object') {
|
|
106
117
|
config.subscription = {
|
|
107
118
|
...config.subscription,
|
|
@@ -132,7 +143,7 @@ export const useSettingsScreenConfig = (
|
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
return config;
|
|
135
|
-
}, [baseSettingsConfig, translations]);
|
|
146
|
+
}, [baseSettingsConfig, translations, featureRequestDataProvider]);
|
|
136
147
|
|
|
137
148
|
const userProfile = useMemo(() => createUserProfileDisplay({
|
|
138
149
|
profileData: userProfileData,
|
|
@@ -133,8 +133,13 @@ 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;
|
|
136
137
|
const featureRequestScreen = createScreenWithProps("FeatureRequest", FeatureRequestScreen, {
|
|
137
|
-
config:
|
|
138
|
+
config: {
|
|
139
|
+
...feedbackConfig,
|
|
140
|
+
translations: translations?.feedback,
|
|
141
|
+
dataProvider: feedbackConfig?.dataProvider,
|
|
142
|
+
},
|
|
138
143
|
texts: {
|
|
139
144
|
feedbackTypes: [
|
|
140
145
|
{ type: 'general', label: translations?.feedbackModal?.types?.general || 'General' },
|
|
@@ -147,6 +152,17 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
|
|
|
147
152
|
descriptionPlaceholder: translations?.feedbackModal?.descriptionPlaceholder || 'Tell us more...',
|
|
148
153
|
submitButton: translations?.feedbackModal?.submitButton || 'Submit',
|
|
149
154
|
submittingButton: translations?.feedbackModal?.submittingButton || 'Submitting...',
|
|
155
|
+
title: translations?.feedbackModal?.title || 'Submit Feedback',
|
|
156
|
+
defaultTitle: (type: string) => {
|
|
157
|
+
const titles: Record<string, string> = {
|
|
158
|
+
general: translations?.feedbackModal?.types?.general || type,
|
|
159
|
+
bug_report: translations?.feedbackModal?.types?.bugReport || type,
|
|
160
|
+
feature_request: translations?.feedbackModal?.types?.featureRequest || type,
|
|
161
|
+
improvement: translations?.feedbackModal?.types?.improvement || type,
|
|
162
|
+
other: translations?.feedbackModal?.types?.other || type,
|
|
163
|
+
};
|
|
164
|
+
return titles[type] || type;
|
|
165
|
+
},
|
|
150
166
|
}
|
|
151
167
|
});
|
|
152
168
|
|
|
@@ -123,6 +123,31 @@ export interface SettingsTranslations {
|
|
|
123
123
|
description?: string;
|
|
124
124
|
};
|
|
125
125
|
};
|
|
126
|
+
/** Feature Request Screen translations */
|
|
127
|
+
feedback?: {
|
|
128
|
+
screen_title?: string;
|
|
129
|
+
tabs?: {
|
|
130
|
+
all?: string;
|
|
131
|
+
my?: string;
|
|
132
|
+
roadmap?: string;
|
|
133
|
+
};
|
|
134
|
+
banner?: {
|
|
135
|
+
title?: string;
|
|
136
|
+
subtitle?: string;
|
|
137
|
+
};
|
|
138
|
+
trending?: string;
|
|
139
|
+
voter_count?: string;
|
|
140
|
+
comment_count?: string;
|
|
141
|
+
new_idea?: string;
|
|
142
|
+
empty?: string;
|
|
143
|
+
status?: {
|
|
144
|
+
planned?: string;
|
|
145
|
+
review?: string;
|
|
146
|
+
completed?: string;
|
|
147
|
+
pending?: string;
|
|
148
|
+
dismissed?: string;
|
|
149
|
+
};
|
|
150
|
+
};
|
|
126
151
|
feedbackModal?: {
|
|
127
152
|
title?: string;
|
|
128
153
|
ratingLabel?: string;
|
|
@@ -5,6 +5,7 @@
|
|
|
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";
|
|
8
9
|
import type { FAQCategory } from "../../../domains/faqs/domain/entities/FAQEntity";
|
|
9
10
|
import type { SettingsStackParamList } from "../../navigation/types";
|
|
10
11
|
|
|
@@ -44,6 +45,8 @@ export interface FeedbackConfig {
|
|
|
44
45
|
onSubmit?: (data: { type: FeedbackType; rating: number; description: string; title: string }) => Promise<void>;
|
|
45
46
|
/** Custom handler to open feedback screen (overrides default modal) */
|
|
46
47
|
onPress?: () => void;
|
|
48
|
+
/** Data provider for feature request screen (fetching, voting, submitting) */
|
|
49
|
+
dataProvider?: FeatureRequestDataProvider;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/**
|