handle notification type and navigate using deeplinking
This commit is contained in:
@@ -5,23 +5,23 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|||||||
import { PaperProvider } from 'react-native-paper';
|
import { PaperProvider } from 'react-native-paper';
|
||||||
import AppNavigator from './src/navigation/AppNavigator';
|
import AppNavigator from './src/navigation/AppNavigator';
|
||||||
import notifee from '@notifee/react-native';
|
import notifee from '@notifee/react-native';
|
||||||
import FirebaseService from './src/utils/firebase/firebaseServices';
|
import { backgroundHandler, foregroundListener, requestPermission, tokenRefreshHandler } from './src/utils/firebase/firebaseServices';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
const isDarkMode = useColorScheme() === 'dark';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 🔹 Ask for permission
|
// 🔹 Ask for permission
|
||||||
FirebaseService.requestPermission();
|
requestPermission();
|
||||||
|
|
||||||
// 🔹 Handle background + quit state
|
// 🔹 Handle background + quit state
|
||||||
FirebaseService.backgroundHandler();
|
backgroundHandler();
|
||||||
|
|
||||||
// 🔹 Listen for foreground messages
|
// 🔹 Listen for foreground messages
|
||||||
const unsubscribe = FirebaseService.foregroundListener();
|
const unsubscribe = foregroundListener();
|
||||||
|
|
||||||
// 🔹 Listen for token refresh
|
// 🔹 Listen for token refresh
|
||||||
const unsubscribeRefresh = FirebaseService.tokenRefreshHandler();
|
const unsubscribeRefresh = tokenRefreshHandler();
|
||||||
|
|
||||||
// 🔹 Create default notification channel (Android)
|
// 🔹 Create default notification channel (Android)
|
||||||
notifee.createChannel({
|
notifee.createChannel({
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress })
|
|||||||
const navigation = useNavigation<any>()
|
const navigation = useNavigation<any>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.card} onPress={()=> navigation.navigate("GroupDetails", {item, color:colors.Settings})}>
|
<TouchableOpacity style={styles.card} onPress={()=> navigation.navigate("GroupDetails", {item, color:colors.black})}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.groupName}>{item.groupName}</Text>
|
<Text style={styles.groupName}>{item.groupName}</Text>
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ const styles = StyleSheet.create({
|
|||||||
shadowColor: "#000",
|
shadowColor: "#000",
|
||||||
shadowOpacity: 0.05,
|
shadowOpacity: 0.05,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
|
marginHorizontal:12
|
||||||
},
|
},
|
||||||
cardHeader: {
|
cardHeader: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export const RequestCard: FC<RequestCardProps> = ({ item, onApprove, onDecline }
|
|||||||
{/* Bottom section with action buttons */}
|
{/* Bottom section with action buttons */}
|
||||||
<View style={styles.actionsSection}>
|
<View style={styles.actionsSection}>
|
||||||
<TouchableOpacity style={[styles.actionButton, styles.declineButton]} onPress={() => onDecline(item)}>
|
<TouchableOpacity style={[styles.actionButton, styles.declineButton]} onPress={() => onDecline(item)}>
|
||||||
<Ionicons name="close-outline" size={20} color={colors.pastelOrange} />
|
<Ionicons name="close-outline" size={20} color={colors.black} />
|
||||||
<Text style={[styles.actionButtonText, { color: colors.pastelOrange }]}>Decline</Text>
|
<Text style={[styles.actionButtonText,]}>Decline</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={[styles.actionButton, styles.approveButton]} onPress={() => onApprove(item)}>
|
<TouchableOpacity style={[styles.actionButton, styles.approveButton]} onPress={() => onApprove(item)}>
|
||||||
{/* Corrected icon color and updated text color for better contrast */}
|
{/* Corrected icon color and updated text color for better contrast */}
|
||||||
@@ -54,10 +54,11 @@ export const RequestCard: FC<RequestCardProps> = ({ item, onApprove, onDecline }
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
|
marginHorizontal: 12,
|
||||||
backgroundColor: colors.white,
|
backgroundColor: colors.white,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
marginVertical: 8,
|
marginVertical: 8,
|
||||||
elevation: 4,
|
elevation: 5,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 10,
|
shadowRadius: 10,
|
||||||
@@ -67,9 +68,7 @@ const styles = StyleSheet.create({
|
|||||||
userInfoSection: {
|
userInfoSection: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderBottomWidth: 1,
|
paddingBottom: 6,
|
||||||
borderBottomColor: colors.textLight,
|
|
||||||
paddingBottom: 12,
|
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
width: 50,
|
width: 50,
|
||||||
@@ -96,7 +95,6 @@ const styles = StyleSheet.create({
|
|||||||
requestText: {
|
requestText: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: colors.textDark,
|
color: colors.textDark,
|
||||||
lineHeight: 22,
|
|
||||||
},
|
},
|
||||||
groupName: {
|
groupName: {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
@@ -116,11 +114,13 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
},
|
},
|
||||||
declineButton: {
|
declineButton: {
|
||||||
backgroundColor: colors.pastelPink,
|
elevation:5,
|
||||||
|
backgroundColor:colors.white
|
||||||
},
|
},
|
||||||
approveButton: {
|
approveButton: {
|
||||||
// Set background color as requested
|
// Set background color as requested
|
||||||
backgroundColor: colors.pastelOrange,
|
backgroundColor: colors.pastelOrange,
|
||||||
|
elevation:5,
|
||||||
},
|
},
|
||||||
actionButtonText: {
|
actionButtonText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ type CustomModalProps = {
|
|||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
onConfirm?: () => void;
|
onConfirm?: () => void;
|
||||||
confirmButton?:string
|
confirmButton?:string
|
||||||
|
confirmTextColor?:string
|
||||||
|
elevation?:number
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomModal: React.FC<CustomModalProps> = ({
|
const CustomModal: React.FC<CustomModalProps> = ({
|
||||||
@@ -31,6 +33,8 @@ const CustomModal: React.FC<CustomModalProps> = ({
|
|||||||
confirmText = "Confirm",
|
confirmText = "Confirm",
|
||||||
cancelText = "Cancel",
|
cancelText = "Cancel",
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
confirmTextColor,
|
||||||
|
elevation,
|
||||||
confirmButton = colors.primary
|
confirmButton = colors.primary
|
||||||
}) => {
|
}) => {
|
||||||
const slideAnim = useRef(new Animated.Value(height)).current; // starts off screen
|
const slideAnim = useRef(new Animated.Value(height)).current; // starts off screen
|
||||||
@@ -89,17 +93,17 @@ const CustomModal: React.FC<CustomModalProps> = ({
|
|||||||
{/* Footer Actions */}
|
{/* Footer Actions */}
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.button, styles.cancelButton]}
|
style={[styles.button, styles.cancelButton,{elevation:elevation}]}
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
>
|
>
|
||||||
<Text style={styles.cancelText}>{cancelText}</Text>
|
<Text style={styles.cancelText}>{cancelText}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.button, {backgroundColor:confirmButton}]}
|
style={[styles.button, {backgroundColor:confirmButton ,elevation:elevation}]}
|
||||||
onPress={onConfirm}
|
onPress={onConfirm}
|
||||||
>
|
>
|
||||||
<Text style={styles.confirmText}>{confirmText}</Text>
|
<Text style={[styles.confirmText,{color:confirmTextColor}]}>{confirmText}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ import ManagementTabNavigation from "../screens/management/ManagementTabNavigati
|
|||||||
import TransactionsList from "../screens/transaction/TransactionList";
|
import TransactionsList from "../screens/transaction/TransactionList";
|
||||||
import RandomSelectUserScreen from "../screens/management/RandomSelectUserScreen";
|
import RandomSelectUserScreen from "../screens/management/RandomSelectUserScreen";
|
||||||
import Notification from "../screens/notification/Notification";
|
import Notification from "../screens/notification/Notification";
|
||||||
|
import ManagementScreen from "../screens/management/ManagementScreen";
|
||||||
|
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
Splash: undefined;
|
Splash: undefined;
|
||||||
Login: undefined;
|
Login: undefined;
|
||||||
OtpVarify: { mobile: string };
|
OtpVarify: { mobile: string };
|
||||||
Home: undefined;
|
Home: undefined;
|
||||||
HomeTabs: undefined;
|
|
||||||
OrganizerRegister: undefined;
|
OrganizerRegister: undefined;
|
||||||
Profile: undefined;
|
Profile: undefined;
|
||||||
GroupCreation: undefined;
|
GroupCreation: undefined;
|
||||||
@@ -43,7 +43,12 @@ export type RootStackParamList = {
|
|||||||
ManagementTabs: { groupId: string | number; }
|
ManagementTabs: { groupId: string | number; }
|
||||||
TransactionList: undefined
|
TransactionList: undefined
|
||||||
RandomSelectUserScreen: { groupId: string | number, cycleNumber: number };
|
RandomSelectUserScreen: { groupId: string | number, cycleNumber: number };
|
||||||
Notification:undefined
|
Notification: undefined;
|
||||||
|
Management:
|
||||||
|
| { actionTab: "My Groups" }
|
||||||
|
| { actionTab1: "Joined Groups" }
|
||||||
|
| { actionTab2: "Requests" }
|
||||||
|
| { actionTab3: "Your Requests" };
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||||
@@ -67,9 +72,16 @@ const linking = {
|
|||||||
code: (code: string | number) => `${code}`,
|
code: (code: string | number) => `${code}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Add deep linking for notification screens
|
||||||
|
ContributionDetails: "contribution/:contributionId",
|
||||||
|
GroupDetails: "group/:groupId",
|
||||||
|
CycleCompleted: "cycle/completed/:groupId/:cycleNumber",
|
||||||
|
// Add other notification screens as needed
|
||||||
|
// Add other notification screens as needed
|
||||||
// Other screens in your navigator can be defined here
|
// Other screens in your navigator can be defined here
|
||||||
// e.g., GroupDetails: "group/:id"
|
// e.g., GroupDetails: "group/:id"
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -98,6 +110,7 @@ const linking = {
|
|||||||
<Stack.Screen name="TransactionList" component={TransactionsList} options={{ headerShown: false }} />
|
<Stack.Screen name="TransactionList" component={TransactionsList} options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="RandomSelectUserScreen" component={RandomSelectUserScreen} options={{ headerShown: false }} />
|
<Stack.Screen name="RandomSelectUserScreen" component={RandomSelectUserScreen} options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="Notification" component={Notification} options={{ headerShown: false }} />
|
<Stack.Screen name="Notification" component={Notification} options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="Management" component={ManagementScreen} options={{ headerShown: false }} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
// Define the shape of the notification data object
|
||||||
|
export interface NotificationData {
|
||||||
|
msgType: string;
|
||||||
|
body: string;
|
||||||
|
title: string;
|
||||||
|
additionalProp1?: string;
|
||||||
|
additionalProp2?: string;
|
||||||
|
[key: string]: any; // For any additional properties
|
||||||
|
}
|
||||||
|
|
||||||
// Define the shape of a notification object
|
// Define the shape of a notification object
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
originalPriority: number;
|
||||||
message: string;
|
priority: number;
|
||||||
time: string;
|
sentTime: number;
|
||||||
|
data: NotificationData;
|
||||||
|
from: string;
|
||||||
|
messageId: string;
|
||||||
|
ttl: number;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,28 +37,59 @@ const notificationSlice = createSlice({
|
|||||||
name: 'notifications',
|
name: 'notifications',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
addNotification: (state, action: PayloadAction<Notification>) => {
|
addNotification: (state, action: PayloadAction<Omit<Notification, 'read'>>) => {
|
||||||
// Adds the new notification to the top
|
// Create a new notification with read status and add to the top
|
||||||
state.notifications.unshift(action.payload);
|
const newNotification: Notification = {
|
||||||
|
...action.payload,
|
||||||
|
read: false,
|
||||||
|
};
|
||||||
|
state.notifications.unshift(newNotification);
|
||||||
},
|
},
|
||||||
|
|
||||||
removeNotification: (state, action: PayloadAction<string>) => {
|
removeNotification: (state, action: PayloadAction<string>) => {
|
||||||
// Remove notification by id
|
// Remove notification by id (using messageId as id)
|
||||||
state.notifications = state.notifications.filter(
|
state.notifications = state.notifications.filter(
|
||||||
(item) => item.id !== action.payload
|
(item) => item.messageId !== action.payload
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearAllNotifications: (state) => {
|
clearAllNotifications: (state) => {
|
||||||
state.notifications = [];
|
state.notifications = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
markAsRead: (state, action: PayloadAction<string>) => {
|
markAsRead: (state, action: PayloadAction<string>) => {
|
||||||
// Mark one notification as read
|
// Mark one notification as read by messageId
|
||||||
const notification = state.notifications.find(
|
const notification = state.notifications.find(
|
||||||
(item) => item.id === action.payload
|
(item) => item.messageId === action.payload
|
||||||
);
|
);
|
||||||
if (notification) {
|
if (notification) {
|
||||||
notification.read = true;
|
notification.read = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
markAllAsRead: (state) => {
|
||||||
|
// Mark all notifications as read
|
||||||
|
state.notifications.forEach(notification => {
|
||||||
|
notification.read = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Additional utility actions that might be useful
|
||||||
|
removeExpiredNotifications: (state) => {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
state.notifications = state.notifications.filter(
|
||||||
|
(notification) => currentTime - notification.sentTime < notification.ttl * 1000
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateNotificationPriority: (state, action: PayloadAction<{ messageId: string; priority: number }>) => {
|
||||||
|
const notification = state.notifications.find(
|
||||||
|
(item) => item.messageId === action.payload.messageId
|
||||||
|
);
|
||||||
|
if (notification) {
|
||||||
|
notification.priority = action.payload.priority;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,6 +98,9 @@ export const {
|
|||||||
removeNotification,
|
removeNotification,
|
||||||
clearAllNotifications,
|
clearAllNotifications,
|
||||||
markAsRead,
|
markAsRead,
|
||||||
|
markAllAsRead,
|
||||||
|
removeExpiredNotifications,
|
||||||
|
updateNotificationPriority,
|
||||||
} = notificationSlice.actions;
|
} = notificationSlice.actions;
|
||||||
|
|
||||||
export default notificationSlice.reducer;
|
export default notificationSlice.reducer;
|
||||||
@@ -56,7 +56,7 @@ const ContributionDetails: React.FC<ContributionProps> = ({
|
|||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomModal visible={visible} onClose={onClose} onConfirm={onConfirm}>
|
<CustomModal visible={visible} onClose={onClose} onConfirm={onConfirm} confirmButton={colors.pastelOrange} confirmTextColor={colors.black} elevation={5}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
@@ -64,7 +64,7 @@ const ContributionDetails: React.FC<ContributionProps> = ({
|
|||||||
{ opacity: fadeAnim, transform: [{ translateY: slideAnim }] },
|
{ opacity: fadeAnim, transform: [{ translateY: slideAnim }] },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Ionicons name="wallet-outline" size={32} color={colors.primary} />
|
<Ionicons name="wallet-outline" size={32} color={colors.black} />
|
||||||
<Text style={styles.headerTitle}>Contribution Summary</Text>
|
<Text style={styles.headerTitle}>Contribution Summary</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
@@ -126,7 +126,6 @@ const styles = StyleSheet.create({
|
|||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
color: colors.primary,
|
|
||||||
marginLeft: 10,
|
marginLeft: 10,
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
@@ -154,7 +153,7 @@ const styles = StyleSheet.create({
|
|||||||
valueHighlight: {
|
valueHighlight: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
color: colors.primary,
|
color: colors.black,
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
|
|||||||
try {
|
try {
|
||||||
// Replace with your actual API call to join the group
|
// Replace with your actual API call to join the group
|
||||||
// For now, it just shows an alert.
|
// For now, it just shows an alert.
|
||||||
await apiCall(`https://api.godemo.in/FundCollect/api/fund/groups/join/invite/${code}`, 'POST');
|
await apiCall(`http://192.168.1.5:9060/api/fund/groups/join/invite/${code}`, 'POST');
|
||||||
Alert.alert('Success!', `You have requested to join "${groupDetails.groupName}".`);
|
Alert.alert('Success!', `You have requested to join "${groupDetails.groupName}".`);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Alert.alert('Join Error', e.message || 'An unexpected error occurred.');
|
Alert.alert('Join Error', e.message || 'An unexpected error occurred.');
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export default function LoginScreen() {
|
|||||||
if (mobile.length === 10) {
|
if (mobile.length === 10) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
console.log("Login api call:");
|
||||||
// FIX: Used the imported LOGIN_URL constant instead of a string literal
|
// FIX: Used the imported LOGIN_URL constant instead of a string literal
|
||||||
const res = await axios.get(LOGIN_URL, { params: { mobile } });
|
const res = await axios.get(LOGIN_URL, { params: { mobile } });
|
||||||
console.log("Login response:", res.data);
|
console.log("Login response:", res.data);
|
||||||
@@ -76,6 +77,7 @@ export default function LoginScreen() {
|
|||||||
[{ text: "Try Again", onPress: hideAlert }],
|
[{ text: "Try Again", onPress: hideAlert }],
|
||||||
'error'
|
'error'
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Login error:", error);
|
console.error("Login error:", error);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls";
|
|||||||
import colors from "../components/colors";
|
import colors from "../components/colors";
|
||||||
import LinearGradient from "react-native-linear-gradient";
|
import LinearGradient from "react-native-linear-gradient";
|
||||||
import CustomAlert from "../components/CostomAlart";
|
import CustomAlert from "../components/CostomAlart";
|
||||||
import FirebaseService from "../utils/firebase/firebaseServices";
|
import { getFcmToken } from "../utils/firebase/firebaseServices";
|
||||||
|
|
||||||
const CELL_COUNT = 6;
|
const CELL_COUNT = 6;
|
||||||
type OtpVerifyRouteProp = RouteProp<RootStackParamList, "OtpVarify">;
|
type OtpVerifyRouteProp = RouteProp<RootStackParamList, "OtpVarify">;
|
||||||
@@ -128,7 +128,7 @@ export default function OtpVerificationScreen() {
|
|||||||
const { jwt } = res.data?.data || {};
|
const { jwt } = res.data?.data || {};
|
||||||
console.log("✅ OTP Verified:", res.data);
|
console.log("✅ OTP Verified:", res.data);
|
||||||
await saveToken(jwt);
|
await saveToken(jwt);
|
||||||
await FirebaseService.getFcmToken(res.data?.data?.userId);
|
await getFcmToken(res.data?.data?.userId);
|
||||||
|
|
||||||
|
|
||||||
let hasActionRun = false;
|
let hasActionRun = false;
|
||||||
@@ -136,7 +136,7 @@ export default function OtpVerificationScreen() {
|
|||||||
if (hasActionRun) return;
|
if (hasActionRun) return;
|
||||||
hasActionRun = true;
|
hasActionRun = true;
|
||||||
hideAlert();
|
hideAlert();
|
||||||
navigation.navigate("Home");
|
navigation.replace("Home");
|
||||||
};
|
};
|
||||||
|
|
||||||
triggerAlert(
|
triggerAlert(
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ const GroupList = () => {
|
|||||||
}
|
}
|
||||||
}, [search, groups]);
|
}, [search, groups]);
|
||||||
|
|
||||||
if (loading) return <ActivityIndicator size="large" color={colors.Home} />;
|
if (loading) return <ActivityIndicator size="large" color={colors.pastelBlue} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -175,10 +175,10 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
},
|
},
|
||||||
avatarText: { fontSize: 18, fontWeight: "700", color: colors.Home },
|
avatarText: { fontSize: 18, fontWeight: "700", color: colors.black },
|
||||||
groupName: { fontSize: 16, fontWeight: "600", color: colors.textDark },
|
groupName: { fontSize: 16, fontWeight: "600", color: colors.textDark },
|
||||||
groupDescription: { fontSize: 13, color: colors.textLight, marginTop: 2 },
|
groupDescription: { fontSize: 13, color: colors.textLight, marginTop: 2 },
|
||||||
groupMeta: { fontSize: 12, color: colors.Home, marginTop: 2 },
|
groupMeta: { fontSize: 12, color: colors.black, marginTop: 2 },
|
||||||
detailsBox: {
|
detailsBox: {
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
padding: 10,
|
padding: 10,
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ const userActions: ActionItem[] = [
|
|||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const user = useSelector((state: any) => state.user);
|
const user = useSelector((state: any) => state.user);
|
||||||
|
const notifications = useSelector(
|
||||||
|
(state: any) => state.notifications.notifications
|
||||||
|
);
|
||||||
const navigation = useNavigation<TabNavigationProp>();
|
const navigation = useNavigation<TabNavigationProp>();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -98,6 +101,8 @@ const Home: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const unreadCount = notifications.filter((item: any) => !item.read).length;
|
||||||
|
|
||||||
const renderGridItem: ListRenderItem<ActionItem> = ({ item }) => (
|
const renderGridItem: ListRenderItem<ActionItem> = ({ item }) => (
|
||||||
<View style={styles.cardWrapper}>
|
<View style={styles.cardWrapper}>
|
||||||
<AnimatedCard
|
<AnimatedCard
|
||||||
@@ -126,6 +131,7 @@ const Home: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={() => navigation.navigate("Notification" as never)}>
|
<TouchableOpacity onPress={() => navigation.navigate("Notification" as never)}>
|
||||||
{/* <Image source={getProfileImageUri()} style={styles.profileImage} /> */}
|
{/* <Image source={getProfileImageUri()} style={styles.profileImage} /> */}
|
||||||
|
<Text style={{position: 'absolute', right: -6, top: -4, backgroundColor: colors.danger, color: colors.white, borderRadius: 8, width: 16, height: 16, textAlign: 'center', fontSize: 10, fontWeight: 'bold', lineHeight: 16, zIndex: 1}}>{unreadCount > 0 ? unreadCount : ''}</Text>
|
||||||
<Ionicons name="notifications-outline" size={30} color={colors.textDark} />
|
<Ionicons name="notifications-outline" size={30} color={colors.textDark} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ const RequestList: React.FC = () => {
|
|||||||
|
|
||||||
// Make API call to approve the request
|
// Make API call to approve the request
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://api.godemo.in/FundCollect/api/fund/requests/${requestId}/approve`,
|
`http://192.168.1.5:9060/api/fund/requests/${requestId}/approve`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -171,7 +171,7 @@ const RequestList: React.FC = () => {
|
|||||||
|
|
||||||
// Make API call to decline the request
|
// Make API call to decline the request
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://api.godemo.in/FundCollect/api/fund/requests/${requestedId}/reject?reason=/${reason}`,
|
`http://192.168.1.5:9060/api/fund/requests/${requestedId}/reject?reason=/${reason}`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
|
import { markAsRead } from '../../redux/notificationSlice';
|
||||||
|
|
||||||
|
// Define the navigation prop type
|
||||||
|
type NotificationNavigationProp = StackNavigationProp<RootStackParamList>;
|
||||||
|
|
||||||
|
const getNotificationTypeNavigateScreen = (msgType: string): keyof RootStackParamList => {
|
||||||
|
switch (msgType) {
|
||||||
|
case 'JOIN_REQUEST':
|
||||||
|
return 'Management'; // Navigate to management for join requests
|
||||||
|
case 'CONTRIBUTION':
|
||||||
|
return 'GroupDetails';
|
||||||
|
case 'CONTRIBUTION_ACK':
|
||||||
|
return 'GroupDetails';
|
||||||
|
case 'CYCLE_COMPLETED':
|
||||||
|
return 'GroupDetails';
|
||||||
|
case 'WITHDRAWAL':
|
||||||
|
return 'GroupDetails';
|
||||||
|
case 'WITHDRAWAL_NOTIFY':
|
||||||
|
return 'GroupDetails';
|
||||||
|
case 'CYCLE_WAIVER':
|
||||||
|
return 'GroupDetails';
|
||||||
|
case 'WAIVER_RECEIVED':
|
||||||
|
return 'GroupDetails';
|
||||||
|
case 'USER_SELECTED':
|
||||||
|
return 'GroupDetails';
|
||||||
|
case 'GROUP_STARTED':
|
||||||
|
return 'GroupDetails';
|
||||||
|
case 'JOIN_APPROVED':
|
||||||
|
return 'Management';
|
||||||
|
case 'JOIN_VIA_INVITE':
|
||||||
|
return 'Management';
|
||||||
|
default:
|
||||||
|
return 'Home';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// In your notification component
|
||||||
|
export const useNotificationHandler = () => {
|
||||||
|
const navigation = useNavigation<NotificationNavigationProp>();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleMarkAsReadAndNavigate = async (
|
||||||
|
messageId: string,
|
||||||
|
msgType: string,
|
||||||
|
additionalParams: any = {}
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
console.log("msgType : ", msgType);
|
||||||
|
// First mark as read
|
||||||
|
await dispatch(markAsRead(messageId));
|
||||||
|
|
||||||
|
// Then navigate to the appropriate screen
|
||||||
|
const screenName = getNotificationTypeNavigateScreen(msgType);
|
||||||
|
|
||||||
|
// Prepare navigation params based on screen and additional data
|
||||||
|
const navigationParams: any = { ...additionalParams };
|
||||||
|
|
||||||
|
// Navigate to the screen
|
||||||
|
navigation.navigate(screenName, navigationParams);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling notification:', error);
|
||||||
|
// Fallback navigation if marking as read fails
|
||||||
|
const screenName = getNotificationTypeNavigateScreen(msgType);
|
||||||
|
navigation.navigate(screenName, additionalParams);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleMarkAsReadAndNavigate };
|
||||||
|
};
|
||||||
@@ -28,7 +28,7 @@ import *as Keychain from 'react-native-keychain'
|
|||||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||||
import { CustomButton } from '../../components/common/Custom';
|
import { CustomButton } from '../../components/common/Custom';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import CustomModal from '../../components/common/CustomModal';
|
// import CustomModal from '../../components/common/CustomModal';
|
||||||
import WinnerUser from './WinnerUser';
|
import WinnerUser from './WinnerUser';
|
||||||
|
|
||||||
|
|
||||||
@@ -347,7 +347,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
<Text style={styles.cardTitle}>{title}</Text>
|
<Text style={styles.cardTitle}>{title}</Text>
|
||||||
{isEditable && !editingCard && (
|
{isEditable && !editingCard && (
|
||||||
<TouchableOpacity onPress={() => setEditingCard(cardKey)} style={styles.editButton}>
|
<TouchableOpacity onPress={() => setEditingCard(cardKey)} style={styles.editButton}>
|
||||||
<Text style={styles.editButtonText}>✎</Text>
|
<Text style={styles.editButtonText}>Update</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -538,7 +538,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
onPress={handleUpdateImage}
|
onPress={handleUpdateImage}
|
||||||
icon={imageUpdating ? undefined : "cloud-upload"}
|
icon={imageUpdating ? undefined : "cloud-upload"}
|
||||||
style={{ paddingHorizontal: 20 }}
|
style={{ paddingHorizontal: 20 }}
|
||||||
buttonBackground={colors.pastelBlue}
|
buttonBackground={colors.pastelOrange}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -675,7 +675,7 @@ const styles = StyleSheet.create({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
// right: 0,
|
// right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
backgroundColor: colors.pastelBlue,
|
backgroundColor: colors.pastelOrange,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
@@ -695,8 +695,8 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
},
|
},
|
||||||
cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937' },
|
cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937' },
|
||||||
editButton: { padding: 8 },
|
editButton: { padding: 8,backgroundColor:colors.pastelOrange, borderRadius: 15,elevation:5 },
|
||||||
editButtonText: { color: colors.pastelPink, fontSize: 20, fontWeight: 'bold' },
|
editButtonText: { color: colors.black, fontSize: 15, fontWeight: 'bold' },
|
||||||
fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingBottom: 12, marginBottom: 12 },
|
fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingBottom: 12, marginBottom: 12 },
|
||||||
infoLabel: { fontSize: 16, color: '#6B7280', marginBottom: 4 },
|
infoLabel: { fontSize: 16, color: '#6B7280', marginBottom: 4 },
|
||||||
infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
|
infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
|
||||||
@@ -713,8 +713,8 @@ const styles = StyleSheet.create({
|
|||||||
button: { borderRadius: 10, paddingVertical: 12, paddingHorizontal: 20, marginLeft: 10, minWidth: 80, alignItems: 'center', justifyContent: 'center' },
|
button: { borderRadius: 10, paddingVertical: 12, paddingHorizontal: 20, marginLeft: 10, minWidth: 80, alignItems: 'center', justifyContent: 'center' },
|
||||||
saveButton: { backgroundColor: colors.pastelOrange },
|
saveButton: { backgroundColor: colors.pastelOrange },
|
||||||
cancelButton: { backgroundColor: '#E5E7EB' },
|
cancelButton: { backgroundColor: '#E5E7EB' },
|
||||||
inviteButton: { backgroundColor: colors.primary },
|
inviteButton: { backgroundColor: colors.pastelOrange },
|
||||||
buttonText: { color: '#ffffffff', fontSize: 15, fontWeight: '600' },
|
buttonText: { color: colors.black, fontSize: 15, fontWeight: '600' },
|
||||||
floatingButtonContainer: {
|
floatingButtonContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 85,
|
bottom: 85,
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ const ManagementScreen: FC = () => {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#f5f6fa' },
|
container: { flex: 1, backgroundColor: '#f5f6fa' },
|
||||||
header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 },
|
header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 },
|
||||||
title: { fontSize: 22, fontWeight: "600", color: colors.Settings },
|
title: { fontSize: 22, fontWeight: "600"},
|
||||||
segmentedControl: { flexDirection: 'row',borderRadius: 10, marginHorizontal: 20, marginBottom: 20, padding: 3 },
|
segmentedControl: { flexDirection: 'row',borderRadius: 10,marginBottom: 20, padding: 3 },
|
||||||
segmentButton: { flex: 1, paddingVertical: 12, alignItems: 'center', borderRadius: 8 },
|
segmentButton: { flex: 1, paddingVertical: 12, alignItems: 'center', borderRadius: 8 },
|
||||||
segmentActive: { backgroundColor: colors.pastelOrange, elevation: 5,borderRadius:15, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
|
segmentActive: { backgroundColor: colors.pastelOrange, elevation: 5,borderRadius:15, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
|
||||||
segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' },
|
segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' },
|
||||||
|
|||||||
@@ -1,22 +1,33 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator, BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
||||||
import { View, Text, StyleSheet, Platform, TouchableOpacity } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import {Ionicons} from '@react-native-vector-icons/ionicons'; // Correct import for react-native-vector-icons
|
import { Ionicons } from '@react-native-vector-icons/ionicons';
|
||||||
|
|
||||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
|
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
|
||||||
import EditGroupScreen from './EditGroupScreen'; // Your screen component
|
import colors from '../../components/colors';
|
||||||
|
|
||||||
|
|
||||||
|
// --- Screen Imports ---
|
||||||
|
import EditGroupScreen from './EditGroupScreen';
|
||||||
import Payments from './Payments';
|
import Payments from './Payments';
|
||||||
import Member from './Member';
|
import Member from './Member';
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<RootStackParamList, 'ManagementTabs'>;
|
|
||||||
|
|
||||||
|
// --- Type Definitions ---
|
||||||
|
type Props = NativeStackScreenProps<RootStackParamList, 'ManagementTabs'>;
|
||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator();
|
||||||
|
|
||||||
// --- Custom Floating Tab Bar Component ---
|
|
||||||
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
|
||||||
|
|
||||||
|
// --- Icon Mapping ---
|
||||||
|
const iconMap: { [key: string]: string } = {
|
||||||
|
EditGroup: 'trail-sign-outline',
|
||||||
|
Payments: 'trending-up',
|
||||||
|
Member: 'people-outline',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// --- Custom Floating Tab Bar Component ---
|
||||||
const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) => {
|
const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
@@ -24,14 +35,11 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
|
|||||||
<View style={[styles.tabBarContainer, { bottom: insets.bottom > 0 ? insets.bottom : 16 }]}>
|
<View style={[styles.tabBarContainer, { bottom: insets.bottom > 0 ? insets.bottom : 16 }]}>
|
||||||
{state.routes.map((route, index) => {
|
{state.routes.map((route, index) => {
|
||||||
const { options } = descriptors[route.key];
|
const { options } = descriptors[route.key];
|
||||||
const label = options.tabBarLabel ?? options.title ?? route.name;
|
const label = options.tabBarLabel?.toString() ?? options.title ?? route.name;
|
||||||
|
|
||||||
console.log("option : ", options)
|
|
||||||
// A custom option to pass the icon name
|
|
||||||
const iconName = label === "Settings" ? "trail-sign-outline" : label === "Payments" ? "trending-up" : label === "Members" ? "people-outline" : "flower";
|
|
||||||
|
|
||||||
const isFocused = state.index === index;
|
const isFocused = state.index === index;
|
||||||
|
|
||||||
|
const iconName = iconMap[route.name] || 'help-circle-outline';
|
||||||
|
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
const event = navigation.emit({
|
const event = navigation.emit({
|
||||||
type: 'tabPress',
|
type: 'tabPress',
|
||||||
@@ -60,15 +68,16 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
|
|||||||
testID={options.tabBarButtonTestID}
|
testID={options.tabBarButtonTestID}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
style={styles.tabItem}
|
// The style for the tab item itself no longer needs elevation
|
||||||
|
style={[styles.tabItem, { backgroundColor: isFocused ? colors.pastelOrange : 'transparent' }]}
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={iconName}
|
name={iconName}
|
||||||
size={24}
|
size={24}
|
||||||
color={isFocused ? '#4F46E5' : '#6B7280'}
|
color={isFocused ? colors.black : '#6B7280'}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.tabLabel, { color: isFocused ? '#4F46E5' : '#6B7280' }]}>
|
<Text style={[styles.tabLabel, { color: isFocused ? colors.black : '#6B7280' }]}>
|
||||||
{label as string}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
@@ -77,69 +86,61 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- Main Tab Navigator Component ---
|
// --- Main Tab Navigator Component ---
|
||||||
const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
|
const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
|
||||||
const { groupId } = route.params
|
const { groupId } = route.params;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
// Assign the custom floating tab bar
|
|
||||||
tabBar={props => <FloatingTabBar {...props} />}
|
tabBar={props => <FloatingTabBar {...props} />}
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false, // Hides the default header for all screens in the tab
|
headerShown: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="EditGroup"
|
name="EditGroup"
|
||||||
component={EditGroupScreen}
|
component={EditGroupScreen}
|
||||||
// Pass the groupId to the EditGroupScreen as an initial parameter
|
|
||||||
initialParams={{ groupId }}
|
initialParams={{ groupId }}
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: 'Settings',
|
tabBarLabel: 'Settings',
|
||||||
// Custom prop to define the icon for our FloatingTabBar
|
|
||||||
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Payments"
|
name="Payments"
|
||||||
component={Payments}
|
component={Payments}
|
||||||
// Pass the groupId to the EditGroupScreen as an initial parameter
|
|
||||||
initialParams={{ groupId }}
|
initialParams={{ groupId }}
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: 'Payments',
|
tabBarLabel: 'Payments',
|
||||||
|
|
||||||
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Member"
|
name="Member"
|
||||||
component={Member}
|
component={Member}
|
||||||
// Pass the groupId to the EditGroupScreen as an initial parameter
|
|
||||||
initialParams={{ groupId }}
|
initialParams={{ groupId }}
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: 'Members',
|
tabBarLabel: 'Members',
|
||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
{/* You can easily add more tabs here in the future */}
|
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- Styles ---
|
// --- Styles ---
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tabBarContainer: {
|
tabBarContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 30,
|
left: 20, // Adjusted for better visual balance
|
||||||
right: 30,
|
right: 20, // Adjusted for better visual balance
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
borderRadius: 60,
|
borderRadius: 30,
|
||||||
paddingVertical: 4,
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 8, // Added padding
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
|
// iOS Shadow
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: {
|
shadowOffset: {
|
||||||
width: 0,
|
width: 0,
|
||||||
@@ -147,14 +148,24 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 10,
|
shadowRadius: 10,
|
||||||
|
// Android Shadow
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
},
|
},
|
||||||
tabItem: {
|
tabItem: {
|
||||||
flex: 1,
|
flex: 1, // Allow items to fill the space
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: 8,
|
paddingVertical: 10, // Increased padding for a better touch area
|
||||||
|
borderRadius: 25,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 4,
|
||||||
},
|
},
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 10, // This is for the background color highlight
|
||||||
|
},
|
||||||
|
// The 'activeTab' style is no longer needed as the logic is handled inline
|
||||||
tabLabel: {
|
tabLabel: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
|
|||||||
@@ -128,12 +128,7 @@ const MemberScreen = ({ route }: Props) => {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centered}>
|
<View style={styles.centered}>
|
||||||
<Ionicons name="cloud-offline-outline" size={60} color="#EF4444" />
|
<Text style={styles.errorText}>no members found</Text>
|
||||||
<Text style={styles.errorTitle}>Oops!</Text>
|
|
||||||
<Text style={styles.errorText}>{error}</Text>
|
|
||||||
<TouchableOpacity style={styles.retryButton} onPress={fetchMembers}>
|
|
||||||
<Text style={styles.retryButtonText}>Try Again</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -180,9 +175,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingTop: 20,
|
paddingTop: 20,
|
||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
backgroundColor: 'white',
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#E5E7EB',
|
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { apiCall } from '../../utils/ApiHelper';
|
|||||||
import { BASE_URL } from '../../utils/ApiUrls';
|
import { BASE_URL } from '../../utils/ApiUrls';
|
||||||
|
|
||||||
// --- CONSTANTS ---
|
// --- CONSTANTS ---
|
||||||
const API_BASE_URL = 'https://api.godemo.in/FundCollect/api/payments';
|
const API_BASE_URL = 'http://192.168.1.5:9060/api/payments';
|
||||||
|
|
||||||
// --- TYPE DEFINITIONS ---
|
// --- TYPE DEFINITIONS ---
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
@@ -43,54 +43,61 @@ interface PaymentDetails {
|
|||||||
type PaymentStackParamList = { Payments: { groupId: string } };
|
type PaymentStackParamList = { Payments: { groupId: string } };
|
||||||
type PaymentsRouteProp = RouteProp<PaymentStackParamList, 'Payments'>;
|
type PaymentsRouteProp = RouteProp<PaymentStackParamList, 'Payments'>;
|
||||||
|
|
||||||
|
// The component is a valid React Functional Component, which is required for using hooks.
|
||||||
const Payments: React.FC = () => {
|
const Payments: React.FC = () => {
|
||||||
|
// --- HOOKS ---
|
||||||
|
// All hooks are called at the top level, which is correct.
|
||||||
const route = useRoute<PaymentsRouteProp>();
|
const route = useRoute<PaymentsRouteProp>();
|
||||||
const { groupId } = route.params;
|
const { groupId } = route.params;
|
||||||
|
|
||||||
const [paymentDetails, setPaymentDetails] = useState<PaymentDetails | null>(null);
|
const [paymentDetails, setPaymentDetails] = useState<PaymentDetails | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [isGroupActive, setIsGroupActive] = useState(true);
|
||||||
// State for the waiver modal
|
|
||||||
const [isModalVisible, setModalVisible] = useState(false);
|
const [isModalVisible, setModalVisible] = useState(false);
|
||||||
const [currentParticipant, setCurrentParticipant] = useState<Participant | null>(null);
|
const [currentParticipant, setCurrentParticipant] = useState<Participant | null>(null);
|
||||||
const [waiverAmount, setWaiverAmount] = useState('');
|
const [waiverAmount, setWaiverAmount] = useState('');
|
||||||
const [cycleData, setCycleData] = useState<any>();
|
const [cycleData, setCycleData] = useState<any>();
|
||||||
|
|
||||||
|
// --- FUNCTIONS ---
|
||||||
|
// These are regular functions and do not call hooks inside them, which is correct.
|
||||||
const getCycleData = async () => {
|
const getCycleData = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const response = await apiCall(`${API_BASE_URL}/groups/${groupId}/cycle-dashboard`, 'GET') as ApiResponse<any>;
|
const response = await apiCall(`${API_BASE_URL}/groups/${groupId}/cycle-dashboard`, 'GET') as ApiResponse<any>;
|
||||||
if (response && response.statusCode === 200) {
|
if (response && response.statusCode === 200) {
|
||||||
setCycleData(response.data);
|
setCycleData(response.data);
|
||||||
} else {
|
|
||||||
Alert.alert("Error", response?.message || "Failed to fetch cycle data.");
|
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
Alert.alert("Error", error.message || "An unexpected error occurred.");
|
console.error("Failed to fetch cycle data:", error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPaymentDetails = useCallback(async () => {
|
const fetchPaymentDetails = useCallback(async () => {
|
||||||
if (!refreshing) setLoading(true);
|
if (!refreshing) setLoading(true);
|
||||||
|
setIsGroupActive(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiCall(`${BASE_URL}payments/current/payment/details/${groupId}`, 'GET') as ApiResponse<PaymentDetails>;
|
const response = await apiCall(`${BASE_URL}payments/current/payment/details/${groupId}`, 'GET') as ApiResponse<PaymentDetails>;
|
||||||
|
|
||||||
console.log("payment response :", response)
|
|
||||||
if (response && response.statusCode === 200) {
|
if (response && response.statusCode === 200) {
|
||||||
setPaymentDetails(response.data);
|
setPaymentDetails(response.data);
|
||||||
|
} else if (response && response.statusCode === 400) {
|
||||||
|
setIsGroupActive(false);
|
||||||
|
setPaymentDetails(null);
|
||||||
} else {
|
} else {
|
||||||
Alert.alert("Error", response?.message || "Failed to fetch details.");
|
setIsGroupActive(false);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Alert.alert("Error", error.message || "An unexpected error occurred.");
|
console.log("payment error: ", error);
|
||||||
console.log("payment error : ", error)
|
setIsGroupActive(false);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
}, [groupId, refreshing]);
|
}, [groupId, refreshing]);
|
||||||
|
|
||||||
|
// --- EFFECT HOOK ---
|
||||||
|
// useEffect is correctly used to call functions when the component mounts or dependencies change.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCycleData();
|
getCycleData();
|
||||||
fetchPaymentDetails();
|
fetchPaymentDetails();
|
||||||
@@ -104,18 +111,17 @@ const Payments: React.FC = () => {
|
|||||||
|
|
||||||
const openWaiverDialog = (participant: Participant) => {
|
const openWaiverDialog = (participant: Participant) => {
|
||||||
setCurrentParticipant(participant);
|
setCurrentParticipant(participant);
|
||||||
setWaiverAmount('');
|
setWaiverAmount(participant.lateFee.toString());
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- UPDATED: handleWaiveFee now uses the correct API endpoint ---
|
|
||||||
const handleWaiveFee = async () => {
|
const handleWaiveFee = async () => {
|
||||||
if (!currentParticipant || !paymentDetails) return;
|
if (!currentParticipant || !paymentDetails) return;
|
||||||
|
|
||||||
const amountToWaive = parseFloat(waiverAmount);
|
const amountToWaive = parseFloat(waiverAmount);
|
||||||
|
|
||||||
if (isNaN(amountToWaive) || amountToWaive <= 0) {
|
if (isNaN(amountToWaive) || amountToWaive < 0) {
|
||||||
Alert.alert("Invalid Input", "Please enter a waiver amount greater than zero.");
|
Alert.alert("Invalid Input", "Please enter a valid waiver amount.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,15 +131,13 @@ const Payments: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Construct the exact URL as provided
|
|
||||||
const url = `${BASE_URL}groups/${groupId}/cycles/${paymentDetails.currentCycle}/waive/${currentParticipant.userId}?amountToWaive=${amountToWaive}`;
|
const url = `${BASE_URL}groups/${groupId}/cycles/${paymentDetails.currentCycle}/waive/${currentParticipant.userId}?amountToWaive=${amountToWaive}`;
|
||||||
|
const response = await apiCall(url, 'PUT', {}) as any;
|
||||||
const response = await apiCall(url, 'PUT', {}) as any; // Body is empty as amount is in URL
|
|
||||||
|
|
||||||
if (response && response.statusCode === 200) {
|
if (response && response.statusCode === 200) {
|
||||||
Alert.alert("Success", `₹${amountToWaive} has been waived successfully.`);
|
Alert.alert("Success", `₹${amountToWaive} has been waived successfully.`);
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
onRefresh(); // Refresh data to reflect the change
|
onRefresh();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response?.message || "Failed to apply waiver.");
|
throw new Error(response?.message || "Failed to apply waiver.");
|
||||||
}
|
}
|
||||||
@@ -142,9 +146,10 @@ const Payments: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- RENDER LOGIC ---
|
||||||
const renderParticipant = (participant: Participant) => {
|
const renderParticipant = (participant: Participant) => {
|
||||||
const isPending = participant.status === 'PENDING';
|
const isPending = participant.status === 'PENDING';
|
||||||
const canWaive = isPending && participant.lateFee > 0;
|
const canWaive = isPending && participant.lateFee > 0 && !participant.lateFeeWaived;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={participant.userId} style={styles.participantCard}>
|
<View key={participant.userId} style={styles.participantCard}>
|
||||||
@@ -163,7 +168,7 @@ const Payments: React.FC = () => {
|
|||||||
{isPending && participant.lateFee > 0 && (
|
{isPending && participant.lateFee > 0 && (
|
||||||
<Text style={styles.lateFeeText}>Late Fee: ₹{participant.lateFee}</Text>
|
<Text style={styles.lateFeeText}>Late Fee: ₹{participant.lateFee}</Text>
|
||||||
)}
|
)}
|
||||||
{participant.lateFee === 0 && participant.lateFeeWaived && <Text style={styles.waivedText}>Fee Waived</Text>}
|
{participant.lateFeeWaived && <Text style={styles.waivedText}>Fee Waived</Text>}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.menuContainer}>
|
<View style={styles.menuContainer}>
|
||||||
@@ -210,36 +215,48 @@ const Payments: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{loading && !refreshing ? (
|
||||||
|
<ActivityIndicator style={{marginTop: 50}} size="large" color={colors.primary} />
|
||||||
|
) : !isGroupActive ? (
|
||||||
|
<View style={styles.fullScreenMessage}>
|
||||||
|
<Ionicons name="warning-outline" size={40} color={colors.black} />
|
||||||
|
<Text style={styles.fullScreenMessageText}>
|
||||||
|
This group is not currently active.
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity onPress={onRefresh} style={styles.refreshButton}>
|
||||||
|
<Text style={styles.refreshButtonText}>Tap to Refresh</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||||
>
|
>
|
||||||
{loading && !refreshing ? (
|
{paymentDetails && (
|
||||||
<ActivityIndicator style={{marginTop: 50}} size="large" color={colors.primary} />
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.headerTitle}>{paymentDetails?.groupName}</Text>
|
<Text style={styles.headerTitle}>{paymentDetails.groupName}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.summaryContainer}>
|
<View style={styles.summaryContainer}>
|
||||||
<View style={styles.summaryBox}>
|
<View style={styles.summaryBox}>
|
||||||
<Text style={styles.summaryLabel}>Cycle</Text>
|
<Text style={styles.summaryLabel}>Cycle</Text>
|
||||||
<Text style={styles.summaryValue}>{paymentDetails?.currentCycle}</Text>
|
<Text style={styles.summaryValue}>{paymentDetails.currentCycle}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.summaryBox}>
|
<View style={styles.summaryBox}>
|
||||||
<Text style={styles.summaryLabel}>Contribution</Text>
|
<Text style={styles.summaryLabel}>Contribution</Text>
|
||||||
<Text style={styles.summaryValue}>₹{paymentDetails?.contributionAmount}</Text>
|
<Text style={styles.summaryValue}>₹{paymentDetails.contributionAmount}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.summaryBox}>
|
<View style={styles.summaryBox}>
|
||||||
<Text style={styles.summaryLabel}>Total Due</Text>
|
<Text style={styles.summaryLabel}>Total Due</Text>
|
||||||
<Text style={[styles.summaryValue, { color: colors.danger }]}>₹{paymentDetails?.totalDueWithLateFee}</Text>
|
<Text style={[styles.summaryValue, { color: colors.danger }]}>₹{paymentDetails.totalDueWithLateFee}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.sectionTitle}>Participants</Text>
|
<Text style={styles.sectionTitle}>Participants</Text>
|
||||||
{paymentDetails?.participants.map(renderParticipant)}
|
{paymentDetails.participants.map(renderParticipant)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
)}
|
||||||
</AppView>
|
</AppView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -251,7 +268,7 @@ const styles = StyleSheet.create({
|
|||||||
summaryContainer: { flexDirection: 'row', justifyContent: 'space-around', padding: 20, backgroundColor: '#fff', marginHorizontal: 15, borderRadius: 12, elevation: 2 },
|
summaryContainer: { flexDirection: 'row', justifyContent: 'space-around', padding: 20, backgroundColor: '#fff', marginHorizontal: 15, borderRadius: 12, elevation: 2 },
|
||||||
summaryBox: { alignItems: 'center' },
|
summaryBox: { alignItems: 'center' },
|
||||||
summaryLabel: { fontSize: 14, color: '#6c757d', marginBottom: 4 },
|
summaryLabel: { fontSize: 14, color: '#6c757d', marginBottom: 4 },
|
||||||
summaryValue: { fontSize: 18, fontWeight: '700', color: colors.primary },
|
summaryValue: { fontSize: 18, fontWeight: '700', color: colors.black },
|
||||||
sectionTitle: { fontSize: 20, fontWeight: 'bold', marginLeft: 20, marginTop: 20, marginBottom: 10 },
|
sectionTitle: { fontSize: 20, fontWeight: 'bold', marginLeft: 20, marginTop: 20, marginBottom: 10 },
|
||||||
participantCard: { backgroundColor: '#fff', marginHorizontal: 15, marginBottom: 10, borderRadius: 10, padding: 15, flexDirection: 'row', alignItems: 'center', elevation: 1 },
|
participantCard: { backgroundColor: '#fff', marginHorizontal: 15, marginBottom: 10, borderRadius: 10, padding: 15, flexDirection: 'row', alignItems: 'center', elevation: 1 },
|
||||||
participantInfo: { flex: 1 },
|
participantInfo: { flex: 1 },
|
||||||
@@ -274,6 +291,30 @@ const styles = StyleSheet.create({
|
|||||||
cancelButtonText: { color: '#333', fontWeight: 'bold' },
|
cancelButtonText: { color: '#333', fontWeight: 'bold' },
|
||||||
saveButton: { backgroundColor: colors.primary },
|
saveButton: { backgroundColor: colors.primary },
|
||||||
saveButtonText: { color: 'white', fontWeight: 'bold' },
|
saveButtonText: { color: 'white', fontWeight: 'bold' },
|
||||||
|
fullScreenMessage: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#f4f6f8',
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
fullScreenMessageText: {
|
||||||
|
fontSize: 18,
|
||||||
|
color: '#333',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: 15,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
refreshButton: {
|
||||||
|
marginTop: 20,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
refreshButtonText: {
|
||||||
|
color: colors.black,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Payments;
|
export default Payments;
|
||||||
|
|||||||
@@ -8,86 +8,314 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
addNotification,
|
|
||||||
removeNotification,
|
removeNotification,
|
||||||
clearAllNotifications,
|
clearAllNotifications,
|
||||||
|
markAsRead,
|
||||||
|
markAllAsRead,
|
||||||
} from '../../redux/notificationSlice';
|
} from '../../redux/notificationSlice';
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
import { Ionicons } from '@react-native-vector-icons/ionicons';
|
import { Ionicons } from '@react-native-vector-icons/ionicons';
|
||||||
import AppView from '../../components/AppView';
|
import AppView from '../../components/AppView';
|
||||||
import Header from '../../components/common/Header';
|
import Header from '../../components/common/Header';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { useNotificationHandler } from '../hook/useNotificationHandler';
|
||||||
|
|
||||||
|
type FilterType = 'all' | 'JOIN_REQUEST' | 'CONTRIBUTION' | 'CONTRIBUTION_ACK' |
|
||||||
|
'CYCLE_COMPLETED' | 'WITHDRAWAL' | 'WITHDRAWAL_NOTIFY' |
|
||||||
|
'CYCLE_WAIVER' | 'WAIVER_RECEIVED' | 'USER_SELECTED' |
|
||||||
|
'GROUP_STARTED' | 'JOIN_APPROVED' | 'JOIN_VIA_INVITE';
|
||||||
|
|
||||||
const Notification = () => {
|
const Notification = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const { handleMarkAsReadAndNavigate } = useNotificationHandler();
|
||||||
const notifications = useSelector(
|
const notifications = useSelector(
|
||||||
(state: any) => state.notifications.notifications
|
(state: any) => state.notifications.notifications
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAdd = () => {
|
|
||||||
const newNotification = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
title: 'New Message',
|
|
||||||
message: 'You have a new message in your inbox.',
|
|
||||||
time: new Date().toLocaleString(),
|
|
||||||
read: false,
|
|
||||||
};
|
|
||||||
dispatch(addNotification(newNotification));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
console.log("notifications ==>", notifications);
|
||||||
dispatch(removeNotification(id));
|
|
||||||
|
const handleDelete = (messageId: string) => {
|
||||||
|
dispatch(removeNotification(messageId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearAll = () => {
|
const handleClearAll = () => {
|
||||||
dispatch(clearAllNotifications());
|
dispatch(clearAllNotifications());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMarkAsRead = (messageId: string, screen: string) => {
|
||||||
|
console.log("screen type : ", screen);
|
||||||
|
handleMarkAsReadAndNavigate(
|
||||||
|
messageId,
|
||||||
|
screen,
|
||||||
|
{
|
||||||
|
// Pass additional parameters based on notification type
|
||||||
|
groupId: 'ebc27862-acde-4129-b12c-fd4c2966e858',
|
||||||
|
cycleNumber: '1',
|
||||||
|
contributionId: '12345',
|
||||||
|
actionTab3: 'Your Requests' ,
|
||||||
|
actionTab1: 'Joined Groups',
|
||||||
|
GroupDetails: {
|
||||||
|
item: {}
|
||||||
|
},
|
||||||
|
// etc.
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMarkAllAsRead = () => {
|
||||||
|
dispatch(markAllAsRead());
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (timestamp: number) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
if (diffInHours < 1) {
|
||||||
|
return 'Just now';
|
||||||
|
} else if (diffInHours < 24) {
|
||||||
|
return `${Math.floor(diffInHours)}h ago`;
|
||||||
|
} else {
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNotificationIcon = (msgType: string) => {
|
||||||
|
switch (msgType) {
|
||||||
|
case 'JOIN_REQUEST':
|
||||||
|
return 'person-add-outline';
|
||||||
|
case 'CONTRIBUTION':
|
||||||
|
return 'cash-outline';
|
||||||
|
case 'CONTRIBUTION_ACK':
|
||||||
|
return 'checkmark-done-outline';
|
||||||
|
case 'CYCLE_COMPLETED':
|
||||||
|
return 'trophy-outline';
|
||||||
|
case 'WITHDRAWAL':
|
||||||
|
return 'card-outline';
|
||||||
|
case 'WITHDRAWAL_NOTIFY':
|
||||||
|
return 'notifications-outline';
|
||||||
|
case 'CYCLE_WAIVER':
|
||||||
|
return 'document-text-outline';
|
||||||
|
case 'WAIVER_RECEIVED':
|
||||||
|
return 'download-outline';
|
||||||
|
case 'USER_SELECTED':
|
||||||
|
return 'people-outline';
|
||||||
|
case 'GROUP_STARTED':
|
||||||
|
return 'rocket-outline';
|
||||||
|
case 'JOIN_APPROVED':
|
||||||
|
return 'checkmark-circle-outline';
|
||||||
|
case 'JOIN_VIA_INVITE':
|
||||||
|
return 'gift-outline';
|
||||||
|
default:
|
||||||
|
return 'notifications-outline';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNotificationTypeNavigateScreen = (msgType: string) => {
|
||||||
|
switch (msgType) {
|
||||||
|
case 'JOIN_REQUEST':
|
||||||
|
return 'Home'; // Screen for managing join requests
|
||||||
|
case 'CONTRIBUTION':
|
||||||
|
return 'ContributionDetails'; // Screen for contribution details
|
||||||
|
case 'CONTRIBUTION_ACK':
|
||||||
|
return 'ContributionHistory'; // Screen for contribution history
|
||||||
|
case 'CYCLE_COMPLETED':
|
||||||
|
return 'CycleCompleted'; // Screen showing cycle completion details
|
||||||
|
case 'WITHDRAWAL':
|
||||||
|
return 'WithdrawalHistory'; // Screen for withdrawal history
|
||||||
|
case 'WITHDRAWAL_NOTIFY':
|
||||||
|
return 'WithdrawalDetails'; // Screen for withdrawal notification details
|
||||||
|
case 'CYCLE_WAIVER':
|
||||||
|
return 'WaiverRequests'; // Screen for waiver requests
|
||||||
|
case 'WAIVER_RECEIVED':
|
||||||
|
return 'WaiverDetails'; // Screen for waiver details
|
||||||
|
case 'USER_SELECTED':
|
||||||
|
return 'UserSelection'; // Screen for user selection details
|
||||||
|
case 'GROUP_STARTED':
|
||||||
|
return 'GroupDetails'; // Screen for group details
|
||||||
|
case 'JOIN_APPROVED':
|
||||||
|
return 'JoinApproved'; // Screen for approved join requests
|
||||||
|
case 'JOIN_VIA_INVITE':
|
||||||
|
return 'InviteDetails'; // Screen for invite details
|
||||||
|
default:
|
||||||
|
return 'Home'; // Default fallback screen
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNotificationColor = (msgType: string) => {
|
||||||
|
switch (msgType) {
|
||||||
|
case 'JOIN_REQUEST':
|
||||||
|
return '#FF6B35'; // Orange
|
||||||
|
case 'CONTRIBUTION':
|
||||||
|
return '#4CAF50'; // Green
|
||||||
|
case 'CONTRIBUTION_ACK':
|
||||||
|
return '#2196F3'; // Blue
|
||||||
|
case 'CYCLE_COMPLETED':
|
||||||
|
return '#9C27B0'; // Purple
|
||||||
|
case 'WITHDRAWAL':
|
||||||
|
return '#FF9800'; // Amber
|
||||||
|
case 'WITHDRAWAL_NOTIFY':
|
||||||
|
return '#F44336'; // Red
|
||||||
|
case 'CYCLE_WAIVER':
|
||||||
|
return '#607D8B'; // Blue Grey
|
||||||
|
case 'WAIVER_RECEIVED':
|
||||||
|
return '#009688'; // Teal
|
||||||
|
case 'USER_SELECTED':
|
||||||
|
return '#673AB7'; // Deep Purple
|
||||||
|
case 'GROUP_STARTED':
|
||||||
|
return '#00BCD4'; // Cyan
|
||||||
|
case 'JOIN_APPROVED':
|
||||||
|
return '#4CAF50'; // Green
|
||||||
|
case 'JOIN_VIA_INVITE':
|
||||||
|
return '#FFC107'; // Yellow
|
||||||
|
default:
|
||||||
|
return '#757575'; // Grey
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilterDisplayName = (filter: FilterType) => {
|
||||||
|
switch (filter) {
|
||||||
|
case 'JOIN_REQUEST':
|
||||||
|
return 'Requests';
|
||||||
|
case 'CONTRIBUTION':
|
||||||
|
return 'Contributions';
|
||||||
|
case 'CONTRIBUTION_ACK':
|
||||||
|
return 'Contributions Ack';
|
||||||
|
case 'CYCLE_COMPLETED':
|
||||||
|
return 'Cycle Completed';
|
||||||
|
case 'WITHDRAWAL':
|
||||||
|
return 'Withdrawals';
|
||||||
|
case 'WITHDRAWAL_NOTIFY':
|
||||||
|
return 'Withdrawal Notify';
|
||||||
|
case 'CYCLE_WAIVER':
|
||||||
|
return 'Waivers';
|
||||||
|
case 'WAIVER_RECEIVED':
|
||||||
|
return 'Waiver Received';
|
||||||
|
case 'USER_SELECTED':
|
||||||
|
return 'User Selected';
|
||||||
|
case 'GROUP_STARTED':
|
||||||
|
return 'Group Started';
|
||||||
|
case 'JOIN_APPROVED':
|
||||||
|
return 'Join Approved';
|
||||||
|
case 'JOIN_VIA_INVITE':
|
||||||
|
return 'Invites';
|
||||||
|
default:
|
||||||
|
return 'All';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unreadCount = notifications.filter((item: any) => !item.read).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppView>
|
<AppView>
|
||||||
{/* Header with Add and Clear All */}
|
{/* Header with actions */}
|
||||||
<Header title='Notifications' rightAction={<LinearGradient
|
<Header
|
||||||
|
title={`Notifications ${unreadCount > 0 ? `(${unreadCount})` : ''}`} />
|
||||||
|
<View style={styles.headerActions}>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<LinearGradient
|
||||||
|
colors={['#4CAF50', '#45a049']}
|
||||||
|
style={[styles.headerButton, styles.markReadButton]}>
|
||||||
|
<TouchableOpacity onPress={handleMarkAllAsRead}>
|
||||||
|
<Text style={styles.headerButtonText}>Mark All Read</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</LinearGradient>
|
||||||
|
)}
|
||||||
|
{notifications.length > 0 && (
|
||||||
|
<LinearGradient
|
||||||
colors={['#FF6B6B', '#FF8E53']}
|
colors={['#FF6B6B', '#FF8E53']}
|
||||||
style={styles.clearButtonGradient}>
|
style={styles.headerButton}>
|
||||||
<TouchableOpacity onPress={handleClearAll}>
|
<TouchableOpacity onPress={handleClearAll}>
|
||||||
<Text style={styles.clearButtonText}>Delete All</Text>
|
<Text style={styles.headerButtonText}>Clear All</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</LinearGradient>}
|
</LinearGradient>
|
||||||
/>
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Add Notification Button */}
|
|
||||||
{/* <LinearGradient
|
|
||||||
colors={['#4facfe', '#00f2fe']}
|
|
||||||
style={styles.addButtonGradient}>
|
|
||||||
<TouchableOpacity onPress={handleAdd}>
|
|
||||||
<Text style={styles.addButtonText}>Add Notification</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</LinearGradient> */}
|
|
||||||
|
|
||||||
{/* Notification List */}
|
{/* Notification List */}
|
||||||
<FlatList
|
<FlatList
|
||||||
data={notifications}
|
data={notifications}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.messageId}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.listContainer}
|
contentContainerStyle={styles.listContainer}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleMarkAsRead(item.messageId, item?.data?.msgType)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['#f5f7fa', '#c3cfe2']}
|
colors={item.read ? ['#f8f9fa', '#e9ecef'] : ['#ffffff', '#f8f9fa']}
|
||||||
style={styles.notificationCard}>
|
style={[
|
||||||
|
styles.notificationCard,
|
||||||
|
!item.read && styles.unreadNotification, { borderLeftColor: getNotificationColor(item.data.msgType) }
|
||||||
|
]}>
|
||||||
<View style={styles.notificationContent}>
|
<View style={styles.notificationContent}>
|
||||||
<View style={{ flex: 1 }}>
|
{/* Icon */}
|
||||||
<Text style={styles.title}>{item.title}</Text>
|
<View style={styles.iconContainer}>
|
||||||
<Text style={styles.message}>{item.message}</Text>
|
<Ionicons
|
||||||
<Text style={styles.time}>{item.time}</Text>
|
name={getNotificationIcon(item.data.msgType)}
|
||||||
|
size={24}
|
||||||
|
color={getNotificationColor(item.data.msgType)}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={() => handleDelete(item.id)}>
|
|
||||||
<Ionicons name="trash-outline" size={22} color="#e63946" />
|
{/* Content */}
|
||||||
|
<View style={styles.textContent}>
|
||||||
|
<View style={styles.headerRow}>
|
||||||
|
<Text style={[
|
||||||
|
styles.title,
|
||||||
|
!item.read && styles.unreadTitle
|
||||||
|
]}>
|
||||||
|
{item.data.title}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.time}>
|
||||||
|
{formatTime(item.sentTime)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.message}>
|
||||||
|
{item.data.body}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{item.data.msgType && (
|
||||||
|
<View style={styles.tagContainer}>
|
||||||
|
<Text style={[
|
||||||
|
styles.tag,
|
||||||
|
{ backgroundColor: getNotificationColor(item.data.msgType) }
|
||||||
|
]}>
|
||||||
|
{/* {item.data.msgType} */}
|
||||||
|
{getFilterDisplayName(item.data.msgType as FilterType)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<View style={styles.actionsContainer}>
|
||||||
|
{!item.read && (
|
||||||
|
<View style={styles.unreadIndicator} />
|
||||||
|
)}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleDelete(item.messageId)}
|
||||||
|
style={styles.deleteButton}
|
||||||
|
>
|
||||||
|
<Ionicons name="trash-outline" size={20} color="#e63946" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<Text style={styles.emptyText}>No notifications found.</Text>
|
<View style={styles.emptyContainer}>
|
||||||
|
<Ionicons name="notifications-off-outline" size={64} color="#ccc" />
|
||||||
|
<Text style={styles.emptyText}>No notifications yet</Text>
|
||||||
|
<Text style={styles.emptySubtext}>
|
||||||
|
When you get notifications, they'll appear here
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</AppView>
|
</AppView>
|
||||||
@@ -97,74 +325,130 @@ const Notification = () => {
|
|||||||
export default Notification;
|
export default Notification;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
headerContainer: {
|
headerActions: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
gap: 8,
|
||||||
alignItems: 'center',
|
justifyContent: 'flex-end',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingTop: 12,
|
paddingVertical: 10,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerButton: {
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: '#222',
|
|
||||||
},
|
|
||||||
clearButtonGradient: {
|
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 12,
|
||||||
},
|
minWidth: 80,
|
||||||
clearButtonText: {
|
|
||||||
color: '#fff',
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
addButtonGradient: {
|
|
||||||
margin: 16,
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingVertical: 10,
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
addButtonText: {
|
markReadButton: {
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
headerButtonText: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
fontSize: 16,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
listContainer: {
|
listContainer: {
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingBottom: 30,
|
paddingBottom: 30,
|
||||||
|
paddingTop: 8,
|
||||||
},
|
},
|
||||||
notificationCard: {
|
notificationCard: {
|
||||||
borderRadius: 10,
|
borderRadius: 12,
|
||||||
padding: 12,
|
padding: 16,
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 5,
|
shadowRadius: 5,
|
||||||
elevation: 2,
|
elevation: 3,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderLeftColor: 'transparent',
|
||||||
|
},
|
||||||
|
unreadNotification: {
|
||||||
|
// borderLeftColor: colors.primary,
|
||||||
|
backgroundColor: '#fff',
|
||||||
},
|
},
|
||||||
notificationContent: {
|
notificationContent: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'flex-start',
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
paddingTop: 2,
|
||||||
|
},
|
||||||
|
textContent: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
headerRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontWeight: '700',
|
fontWeight: '600',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#333',
|
color: '#333',
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
unreadTitle: {
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#000',
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#555',
|
color: '#666',
|
||||||
marginTop: 2,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
time: {
|
time: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#888',
|
color: '#999',
|
||||||
marginTop: 4,
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
tagContainer: {
|
||||||
|
marginTop: 6,
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#fff',
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: 'hidden',
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
actionsContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
unreadIndicator: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: '#2196F3',
|
||||||
|
},
|
||||||
|
deleteButton: {
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
emptyContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: 100,
|
||||||
|
paddingHorizontal: 40,
|
||||||
},
|
},
|
||||||
emptyText: {
|
emptyText: {
|
||||||
textAlign: 'center',
|
fontSize: 18,
|
||||||
marginTop: 60,
|
fontWeight: '600',
|
||||||
fontSize: 16,
|
color: '#666',
|
||||||
|
marginTop: 16,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
emptySubtext: {
|
||||||
|
fontSize: 14,
|
||||||
color: '#999',
|
color: '#999',
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -29,7 +29,7 @@ import { clearUser, setUserData } from "../../redux/userSlice";
|
|||||||
import { StackNavigationProp } from "@react-navigation/stack";
|
import { StackNavigationProp } from "@react-navigation/stack";
|
||||||
import { RootStackParamList } from "../../navigation/AppNavigator";
|
import { RootStackParamList } from "../../navigation/AppNavigator";
|
||||||
import { firebase } from "@react-native-firebase/messaging";
|
import { firebase } from "@react-native-firebase/messaging";
|
||||||
import FirebaseService from "../../utils/firebase/firebaseServices";
|
import FirebaseService, { deleteFcmToken } from "../../utils/firebase/firebaseServices";
|
||||||
|
|
||||||
|
|
||||||
type UserProfile = {
|
type UserProfile = {
|
||||||
@@ -157,13 +157,17 @@ const Profile = () => {
|
|||||||
|
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.status === "success") {
|
console.log("result for profile update :", result);
|
||||||
Alert.alert("Success", "Profile updated successfully");
|
// Alert.alert("Success", "Profile updated successfully");
|
||||||
setEditing(false);
|
// setEditing(false);
|
||||||
fetchUserProfile(); // reload updated data
|
// fetchUserProfile();
|
||||||
} else {
|
// if (result.status === "success") {
|
||||||
Alert.alert("Error", "Failed to update profile");
|
// Alert.alert("Success", "Profile updated successfully");
|
||||||
}
|
// setEditing(false);
|
||||||
|
// fetchUserProfile(); // reload updated data
|
||||||
|
// } else {
|
||||||
|
// Alert.alert("Error", "Failed to update profile");
|
||||||
|
// }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Error updating profile:", error);
|
console.error("❌ Error updating profile:", error);
|
||||||
// Alert.alert("Error", "Could not update profile");
|
// Alert.alert("Error", "Could not update profile");
|
||||||
@@ -305,7 +309,7 @@ const Profile = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await FirebaseService.deleteFcmToken();
|
await deleteFcmToken();
|
||||||
|
|
||||||
// Clear stored credentials
|
// Clear stored credentials
|
||||||
await Keychain.resetGenericPassword();
|
await Keychain.resetGenericPassword();
|
||||||
@@ -549,11 +553,13 @@ const Profile = () => {
|
|||||||
|
|
||||||
<CustomButton
|
<CustomButton
|
||||||
title="Edit Bank Details"
|
title="Edit Bank Details"
|
||||||
onPress={() =>{ navigation.navigate("EditBankDetails", {
|
onPress={() => {
|
||||||
|
navigation.navigate("EditBankDetails", {
|
||||||
currentBankName: user?.bankName,
|
currentBankName: user?.bankName,
|
||||||
currentAccountNumber: user?.bankAccountNumber,
|
currentAccountNumber: user?.bankAccountNumber,
|
||||||
currentBranchCode: user?.branchCode,
|
currentBranchCode: user?.branchCode,
|
||||||
})}}
|
})
|
||||||
|
}}
|
||||||
|
|
||||||
icon="create-outline"
|
icon="create-outline"
|
||||||
style={styles.editButton}
|
style={styles.editButton}
|
||||||
|
|||||||
+12
-13
@@ -36,32 +36,31 @@ export async function apiCall<T>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Response Logging ---
|
// --- Response Logging ---
|
||||||
console.log(`--- API Response: [${response}] ${url} ---`);
|
console.log(`--- API Response:${url}///// [${response}] `);
|
||||||
// Uncomment the line below to see the full response data in logs
|
|
||||||
// console.log("Response Data:", JSON.stringify(response.data, null, 2));
|
|
||||||
console.log("---------------------------------------",response);
|
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// Server responded with a status other than 2xx
|
// Server responded with a status other than 2xx
|
||||||
console.error(
|
// console.error(
|
||||||
"API Error:",
|
// "API Error:",
|
||||||
`Status: ${error.response.status}`,
|
// `Status: ${error.response.status}`,
|
||||||
`Data: ${JSON.stringify(error.response.data)}`
|
// `Data: ${JSON.stringify(error.response.data)}`
|
||||||
);
|
// );
|
||||||
if(error.response.status === 400){
|
if(error.response.status === 400){
|
||||||
|
console.log("API Error:9090", url);
|
||||||
ToastAndroid.show(error.response.data.message || responseErrorMessage, ToastAndroid.SHORT)
|
ToastAndroid.show(error.response.data.message || responseErrorMessage, ToastAndroid.SHORT)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
// Request was made but no response received
|
// Request was made but no response received
|
||||||
console.error("API Error: No response received", error.request);
|
// console.error("API Error: No response received", error.request);
|
||||||
} else {
|
} else {
|
||||||
// Something else went wrong while setting up the request
|
// Something else went wrong while setting up the request
|
||||||
console.error("API Error:", error);
|
// console.error("API Error:", error);
|
||||||
}
|
}
|
||||||
console.error("-----------------");
|
console.log(`API : ${url} API Error:`, error);
|
||||||
|
return error.response.status
|
||||||
// Re-throw the error so that the calling function can handle it if needed
|
// Re-throw the error so that the calling function can handle it if needed
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
// export const BASE_URL = "https://api.godemo.in/FundCollect/api/"
|
export const BASE_URL = "https://api.godemo.in/FundCollect/api/"
|
||||||
export const BASE_URL = "http://192.168.1.5:9060/api/"
|
// export const BASE_URL = "http://192.168.1.5:9060/api/"
|
||||||
export const LOGIN_URL = `${BASE_URL}user/generate/otp?mobiles=`
|
export const LOGIN_URL = `${BASE_URL}user/generate/otp?mobiles=`
|
||||||
export const VARIFY_OTP_URL = `${BASE_URL}user/otp/verify`
|
export const VARIFY_OTP_URL = `${BASE_URL}user/otp/verify`
|
||||||
export const ORGRAGISTRATION = `${BASE_URL}organizer/registration/apply`
|
export const ORGRAGISTRATION = `${BASE_URL}organizer/registration/apply`
|
||||||
|
|||||||
@@ -1,108 +1,325 @@
|
|||||||
|
// import messaging from '@react-native-firebase/messaging';
|
||||||
|
// import notifee, { AndroidImportance } from '@notifee/react-native';
|
||||||
|
// import axios from 'axios';
|
||||||
|
// import { apiCall } from '../ApiHelper';
|
||||||
|
// import { REMOVE_FCM_TOKEN, STORE_FCM_TOKEN } from '../ApiUrls';
|
||||||
|
// import { use } from 'react';
|
||||||
|
// import { useDispatch } from 'react-redux';
|
||||||
|
// import { addNotification } from '../../redux/notificationSlice';
|
||||||
|
|
||||||
|
// // 🔹 Request notification permission
|
||||||
|
// export const requestPermission = async () => {
|
||||||
|
// await messaging().requestPermission();
|
||||||
|
// await notifee.requestPermission();
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
// // 🔹 Display local notification
|
||||||
|
// export const displayNotification = async (title: string, body: string) => {
|
||||||
|
// await notifee.displayNotification({
|
||||||
|
// title,
|
||||||
|
// body,
|
||||||
|
// android: {
|
||||||
|
// channelId: 'default',
|
||||||
|
// smallIcon: 'ic_launcher', // ensure you have this icon in android/app/src/main/res/mipmap
|
||||||
|
// importance: AndroidImportance.HIGH,
|
||||||
|
// pressAction: {
|
||||||
|
// id: 'default',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // 🔹 Get & send token to backend
|
||||||
|
// export const getFcmToken = async (userId: string) => {
|
||||||
|
// console.log('FCM Token o');
|
||||||
|
// try {
|
||||||
|
// console.log('FCM Token 1');
|
||||||
|
// const token = await messaging().getToken();
|
||||||
|
// console.log('FCM Token 2');
|
||||||
|
// console.log('FCM Token login:', token);
|
||||||
|
// await apiCall(STORE_FCM_TOKEN + token, 'POST');
|
||||||
|
// console.log('FCM Token sent to backend');
|
||||||
|
// return token;
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Error getting FCM token:', error);
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // 🔹 Delete token from backend
|
||||||
|
// export const deleteFcmToken = async () => {
|
||||||
|
// try {
|
||||||
|
// console.log('FCM Token 1');
|
||||||
|
// const token = await messaging().getToken();
|
||||||
|
// console.log('FCM Token 2');
|
||||||
|
// console.log('fcm token logout -', token);
|
||||||
|
// await apiCall(REMOVE_FCM_TOKEN + token, 'POST');
|
||||||
|
// console.log('FCM Token deleted from backend');
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Error deleting FCM token:', error);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // 🔹 Foreground notifications
|
||||||
|
// export const foregroundListener = () => {
|
||||||
|
// return messaging().onMessage(async remoteMessage => {
|
||||||
|
// console.log('Foreground message:', remoteMessage);
|
||||||
|
// const { messageId, notification, data } = remoteMessage;
|
||||||
|
// if (!remoteMessage) return;
|
||||||
|
// const { title, body } = data || {};
|
||||||
|
// if (remoteMessage) {
|
||||||
|
// await displayNotification(
|
||||||
|
// (title as string) ?? 'New Message',
|
||||||
|
// (body as string) ?? ''
|
||||||
|
// );
|
||||||
|
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // 🔹 Background & Quit state
|
||||||
|
// export const backgroundHandler = () => {
|
||||||
|
// messaging().setBackgroundMessageHandler(async remoteMessage => {
|
||||||
|
// console.log('Background message:', remoteMessage);
|
||||||
|
// const { messageId, notification, data } = remoteMessage;
|
||||||
|
// if (!remoteMessage) return;
|
||||||
|
// const { title, body } = data || {};
|
||||||
|
// if (remoteMessage) {
|
||||||
|
// await displayNotification(
|
||||||
|
// (title as string) ?? 'New Message',
|
||||||
|
// (body as string) ?? ''
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// messaging().onNotificationOpenedApp(remoteMessage => {
|
||||||
|
// console.log('App opened from background:', remoteMessage);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// messaging()
|
||||||
|
// .getInitialNotification()
|
||||||
|
// .then(remoteMessage => {
|
||||||
|
// if (remoteMessage) {
|
||||||
|
// console.log('App opened from quit state:', remoteMessage);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // 🔹 Token auto-refresh
|
||||||
|
// export const tokenRefreshHandler = () => {
|
||||||
|
// return messaging().onTokenRefresh(async token => {
|
||||||
|
// console.log('New refreshed FCM Token:', token);
|
||||||
|
// // 👉 send to backend
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Optional: Export all functions as a single object for easier imports
|
||||||
|
// export default {
|
||||||
|
// requestPermission,
|
||||||
|
// displayNotification,
|
||||||
|
// getFcmToken,
|
||||||
|
// deleteFcmToken,
|
||||||
|
// foregroundListener,
|
||||||
|
// backgroundHandler,
|
||||||
|
// tokenRefreshHandler,
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import messaging from '@react-native-firebase/messaging';
|
import messaging from '@react-native-firebase/messaging';
|
||||||
import notifee, { AndroidImportance } from '@notifee/react-native';
|
import notifee, { AndroidImportance } from '@notifee/react-native';
|
||||||
import axios from 'axios';
|
|
||||||
import { apiCall } from '../ApiHelper';
|
import { apiCall } from '../ApiHelper';
|
||||||
import { REMOVE_FCM_TOKEN, STORE_FCM_TOKEN } from '../ApiUrls';
|
import { REMOVE_FCM_TOKEN, STORE_FCM_TOKEN } from '../ApiUrls';
|
||||||
|
import { store } from '../../redux/store'; // Import store directly
|
||||||
|
import { addNotification } from '../../redux/notificationSlice';
|
||||||
|
|
||||||
// replace with your backend
|
|
||||||
|
|
||||||
class FirebaseService {
|
|
||||||
// 🔹 Request notification permission
|
// 🔹 Request notification permission
|
||||||
static async requestPermission() {
|
export const requestPermission = async () => {
|
||||||
|
try {
|
||||||
await messaging().requestPermission();
|
await messaging().requestPermission();
|
||||||
await notifee.requestPermission();
|
await notifee.requestPermission();
|
||||||
|
console.log('Notification permission granted');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error requesting notification permission:', error);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 🔹 Display local notification
|
// 🔹 Display local notification
|
||||||
static async displayNotification(title: string, body: string) {
|
export const displayNotification = async (title: string, body: string, data?: any) => {
|
||||||
|
try {
|
||||||
await notifee.displayNotification({
|
await notifee.displayNotification({
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
|
data: data || {},
|
||||||
android: {
|
android: {
|
||||||
channelId: 'default',
|
channelId: 'default',
|
||||||
smallIcon: 'ic_launcher', // ensure you have this icon in android/app/src/main/res/mipmap
|
smallIcon: 'ic_launcher',
|
||||||
importance: AndroidImportance.HIGH,
|
importance: AndroidImportance.HIGH,
|
||||||
pressAction: {
|
pressAction: {
|
||||||
id: 'default',
|
id: 'default',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error displaying notification:', error);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 🔹 Get & send token to backend
|
// 🔹 Get & send token to backend
|
||||||
static async getFcmToken(userId: string) {
|
export const getFcmToken = async (userId: string) => {
|
||||||
console.log('FCM Token o');
|
console.log('Getting FCM Token...');
|
||||||
try {
|
try {
|
||||||
console.log('FCM Token 1');
|
|
||||||
const token = await messaging().getToken();
|
const token = await messaging().getToken();
|
||||||
console.log('FCM Token 2');
|
console.log('FCM Token:', token);
|
||||||
console.log('FCM Token login: ', token);
|
|
||||||
await apiCall(STORE_FCM_TOKEN+token, 'POST');
|
// Send token to backend with user ID
|
||||||
console.log('FCM Token sent to backend');
|
await apiCall(STORE_FCM_TOKEN, 'POST', {
|
||||||
|
token,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
console.log('FCM Token sent to backend', token);
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting FCM token:', error);
|
console.error('Error getting FCM token:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// 🔹 Delete token from backend
|
// 🔹 Delete token from backend
|
||||||
static async deleteFcmToken() {
|
export const deleteFcmToken = async (userId: string) => {
|
||||||
try {
|
try {
|
||||||
console.log('FCM Token 1');
|
|
||||||
const token = await messaging().getToken();
|
const token = await messaging().getToken();
|
||||||
console.log('FCM Token 2');
|
console.log('FCM token to delete:', token);
|
||||||
console.log("fcm token logout -",token)
|
|
||||||
await apiCall(REMOVE_FCM_TOKEN+token, 'POST');
|
// Remove token from backend with user ID
|
||||||
|
await apiCall(REMOVE_FCM_TOKEN, 'POST', {
|
||||||
|
token,
|
||||||
|
userId
|
||||||
|
});
|
||||||
console.log('FCM Token deleted from backend');
|
console.log('FCM Token deleted from backend');
|
||||||
|
|
||||||
|
// Optional: Delete token from device
|
||||||
|
await messaging().deleteToken();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting FCM token:', error);
|
console.error('Error deleting FCM token:', error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// 🔹 Process and add notification to Redux
|
||||||
|
const processAndAddNotification = (remoteMessage: any) => {
|
||||||
|
const {
|
||||||
|
messageId,
|
||||||
|
notification,
|
||||||
|
data,
|
||||||
|
from,
|
||||||
|
sentTime,
|
||||||
|
priority,
|
||||||
|
originalPriority
|
||||||
|
} = remoteMessage;
|
||||||
|
|
||||||
|
if (!messageId) return;
|
||||||
|
|
||||||
|
const title = notification?.title || data?.title || 'New Message';
|
||||||
|
const body = notification?.body || data?.body || 'No content';
|
||||||
|
const msgType = data?.msgType || 'General';
|
||||||
|
|
||||||
|
// Create notification object for Redux
|
||||||
|
const notificationPayload = {
|
||||||
|
id: messageId, // Using messageId as id
|
||||||
|
originalPriority: originalPriority || 2,
|
||||||
|
priority: priority || 2,
|
||||||
|
sentTime: sentTime || Date.now(),
|
||||||
|
data: {
|
||||||
|
msgType: msgType,
|
||||||
|
body: body,
|
||||||
|
title: title,
|
||||||
|
additionalProp1: data?.additionalProp1 || '',
|
||||||
|
additionalProp2: data?.additionalProp2 || '',
|
||||||
|
...data // Spread any additional data
|
||||||
|
},
|
||||||
|
from: from || 'unknown',
|
||||||
|
messageId: messageId,
|
||||||
|
ttl: data?.ttl ? parseInt(data.ttl) : 2419200 // Default 28 days
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dispatch to Redux store
|
||||||
|
store.dispatch(addNotification(notificationPayload));
|
||||||
|
console.log('Notification added to Redux store:', notificationPayload);
|
||||||
|
};
|
||||||
|
|
||||||
// 🔹 Foreground notifications
|
// 🔹 Foreground notifications
|
||||||
static foregroundListener() {
|
export const foregroundListener = () => {
|
||||||
return messaging().onMessage(async remoteMessage => {
|
return messaging().onMessage(async remoteMessage => {
|
||||||
console.log('Foreground message:', remoteMessage);
|
console.log('Foreground message:', remoteMessage);
|
||||||
if (remoteMessage.notification) {
|
|
||||||
await this.displayNotification(
|
if (!remoteMessage) return;
|
||||||
remoteMessage.notification.title ?? 'New Message',
|
|
||||||
remoteMessage.notification.body ?? ''
|
const { notification, data } = remoteMessage;
|
||||||
);
|
const title = notification?.title || data?.title || 'New Message';
|
||||||
}
|
const body = notification?.body || data?.body || '';
|
||||||
|
|
||||||
|
// Display local notification
|
||||||
|
await displayNotification(title, body, data);
|
||||||
|
|
||||||
|
// Add to Redux store
|
||||||
|
processAndAddNotification(remoteMessage);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 🔹 Background & Quit state
|
// 🔹 Background & Quit state
|
||||||
static backgroundHandler() {
|
export const backgroundHandler = () => {
|
||||||
|
// Handle background messages
|
||||||
messaging().setBackgroundMessageHandler(async remoteMessage => {
|
messaging().setBackgroundMessageHandler(async remoteMessage => {
|
||||||
console.log('Background message:', remoteMessage);
|
console.log('Background message:', remoteMessage);
|
||||||
if (remoteMessage.notification) {
|
|
||||||
await this.displayNotification(
|
if (!remoteMessage) return;
|
||||||
remoteMessage.notification.title ?? 'New Message',
|
|
||||||
remoteMessage.notification.body ?? ''
|
const { notification, data } = remoteMessage;
|
||||||
);
|
const title = notification?.title || data?.title || 'New Message';
|
||||||
}
|
const body = notification?.body || data?.body || '';
|
||||||
|
|
||||||
|
// Display local notification
|
||||||
|
await displayNotification(title, body, data);
|
||||||
|
|
||||||
|
// Add to Redux store
|
||||||
|
processAndAddNotification(remoteMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle when app is opened from background by tapping notification
|
||||||
messaging().onNotificationOpenedApp(remoteMessage => {
|
messaging().onNotificationOpenedApp(remoteMessage => {
|
||||||
console.log('App opened from background:', remoteMessage);
|
console.log('App opened from background:', remoteMessage);
|
||||||
|
// You can add navigation logic here based on notification data
|
||||||
|
processAndAddNotification(remoteMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle when app is opened from quit state by tapping notification
|
||||||
messaging().getInitialNotification().then(remoteMessage => {
|
messaging().getInitialNotification().then(remoteMessage => {
|
||||||
if (remoteMessage) {
|
if (remoteMessage) {
|
||||||
console.log('App opened from quit state:', remoteMessage);
|
console.log('App opened from quit state:', remoteMessage);
|
||||||
|
processAndAddNotification(remoteMessage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 🔹 Token auto-refresh
|
// 🔹 Token auto-refresh
|
||||||
static tokenRefreshHandler() {
|
export const tokenRefreshHandler = () => {
|
||||||
return messaging().onTokenRefresh(async token => {
|
return messaging().onTokenRefresh(async token => {
|
||||||
console.log('New refreshed FCM Token:', token);
|
console.log('New refreshed FCM Token:', token);
|
||||||
// 👉 send to backend
|
// You can send the new token to your backend here if needed
|
||||||
|
// await apiCall(STORE_FCM_TOKEN, 'POST', { token });
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default FirebaseService;
|
// Optional: Export all functions as a single object for easier imports
|
||||||
|
export default {
|
||||||
|
requestPermission,
|
||||||
|
displayNotification,
|
||||||
|
getFcmToken,
|
||||||
|
deleteFcmToken,
|
||||||
|
foregroundListener,
|
||||||
|
backgroundHandler,
|
||||||
|
tokenRefreshHandler,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user