diff --git a/App.tsx b/App.tsx index ae0e5a4..0de681b 100644 --- a/App.tsx +++ b/App.tsx @@ -5,23 +5,23 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'; import { PaperProvider } from 'react-native-paper'; import AppNavigator from './src/navigation/AppNavigator'; 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() { const isDarkMode = useColorScheme() === 'dark'; useEffect(() => { // 🔹 Ask for permission - FirebaseService.requestPermission(); + requestPermission(); // 🔹 Handle background + quit state - FirebaseService.backgroundHandler(); + backgroundHandler(); // 🔹 Listen for foreground messages - const unsubscribe = FirebaseService.foregroundListener(); + const unsubscribe = foregroundListener(); // 🔹 Listen for token refresh - const unsubscribeRefresh = FirebaseService.tokenRefreshHandler(); + const unsubscribeRefresh = tokenRefreshHandler(); // 🔹 Create default notification channel (Android) notifee.createChannel({ diff --git a/src/components/JoinedGroupCard.tsx b/src/components/JoinedGroupCard.tsx index bcb2433..a742639 100644 --- a/src/components/JoinedGroupCard.tsx +++ b/src/components/JoinedGroupCard.tsx @@ -66,7 +66,7 @@ export const JoinedGroupCard: FC = ({ item, onViewPress }) const navigation = useNavigation() return ( - navigation.navigate("GroupDetails", {item, color:colors.Settings})}> + navigation.navigate("GroupDetails", {item, color:colors.black})}> {/* Header */} {item.groupName} diff --git a/src/components/MyGroupCard.tsx b/src/components/MyGroupCard.tsx index 3521c8b..25e42a2 100644 --- a/src/components/MyGroupCard.tsx +++ b/src/components/MyGroupCard.tsx @@ -94,6 +94,7 @@ const styles = StyleSheet.create({ shadowColor: "#000", shadowOpacity: 0.05, shadowRadius: 4, + marginHorizontal:12 }, cardHeader: { flexDirection: "row", diff --git a/src/components/RequestCard.tsx b/src/components/RequestCard.tsx index ff8a7a2..7d6e385 100644 --- a/src/components/RequestCard.tsx +++ b/src/components/RequestCard.tsx @@ -37,8 +37,8 @@ export const RequestCard: FC = ({ item, onApprove, onDecline } {/* Bottom section with action buttons */} onDecline(item)}> - - Decline + + Decline onApprove(item)}> {/* Corrected icon color and updated text color for better contrast */} @@ -54,10 +54,11 @@ export const RequestCard: FC = ({ item, onApprove, onDecline } const styles = StyleSheet.create({ card: { + marginHorizontal: 12, backgroundColor: colors.white, borderRadius: 16, marginVertical: 8, - elevation: 4, + elevation: 5, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 10, @@ -67,9 +68,7 @@ const styles = StyleSheet.create({ userInfoSection: { flexDirection: 'row', alignItems: 'center', - borderBottomWidth: 1, - borderBottomColor: colors.textLight, - paddingBottom: 12, + paddingBottom: 6, }, avatar: { width: 50, @@ -96,7 +95,6 @@ const styles = StyleSheet.create({ requestText: { fontSize: 15, color: colors.textDark, - lineHeight: 22, }, groupName: { fontWeight: 'bold', @@ -116,11 +114,13 @@ const styles = StyleSheet.create({ borderRadius: 12, }, declineButton: { - backgroundColor: colors.pastelPink, + elevation:5, + backgroundColor:colors.white }, approveButton: { // Set background color as requested backgroundColor: colors.pastelOrange, + elevation:5, }, actionButtonText: { fontSize: 16, diff --git a/src/components/common/CustomModal.tsx b/src/components/common/CustomModal.tsx index c6b2f52..8c9818c 100644 --- a/src/components/common/CustomModal.tsx +++ b/src/components/common/CustomModal.tsx @@ -21,6 +21,8 @@ type CustomModalProps = { cancelText?: string; onConfirm?: () => void; confirmButton?:string + confirmTextColor?:string + elevation?:number }; const CustomModal: React.FC = ({ @@ -31,6 +33,8 @@ const CustomModal: React.FC = ({ confirmText = "Confirm", cancelText = "Cancel", onConfirm, + confirmTextColor, + elevation, confirmButton = colors.primary }) => { const slideAnim = useRef(new Animated.Value(height)).current; // starts off screen @@ -89,17 +93,17 @@ const CustomModal: React.FC = ({ {/* Footer Actions */} {cancelText} - {confirmText} + {confirmText} diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 0352de6..0096fb1 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -21,58 +21,70 @@ import ManagementTabNavigation from "../screens/management/ManagementTabNavigati import TransactionsList from "../screens/transaction/TransactionList"; import RandomSelectUserScreen from "../screens/management/RandomSelectUserScreen"; import Notification from "../screens/notification/Notification"; +import ManagementScreen from "../screens/management/ManagementScreen"; export type RootStackParamList = { Splash: undefined; Login: undefined; OtpVarify: { mobile: string }; Home: undefined; - HomeTabs: undefined; OrganizerRegister: undefined; Profile: undefined; GroupCreation: undefined; GroupReview: undefined; EditGroup: { groupId: string | number }; - GroupDetails: { item:object }; + GroupDetails: { item: object }; BecomeOrg: undefined; YourContacts: undefined; Transaction: undefined; EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null }; ManageJoinedGroup: { groupId: string | number }; Invite: { groupId: string | number; code: string | number }; - ManagementTabs:{ groupId: string | number;} - TransactionList:undefined + ManagementTabs: { groupId: string | number; } + TransactionList: undefined 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(); export default function AppNavigator() { -const linking = { - prefixes: [ - "myapp://", // Your custom scheme - "https://your-production-domain.com" // Your website for Universal Links/App Links - ], - config: { - screens: { - // Define the route for the InviteScreen - Invite: { - // This path matches the part after the "://" and before the "?" - path: "invite", + const linking = { + prefixes: [ + "myapp://", // Your custom scheme + "https://your-production-domain.com" // Your website for Universal Links/App Links + ], + config: { + screens: { + // Define the route for the InviteScreen + Invite: { + // This path matches the part after the "://" and before the "?" + path: "invite", - // This block tells React Navigation how to handle the query parameters - parse: { - groupId: (groupId: string | number) => `${groupId}`, - code: (code: string | number) => `${code}`, + // This block tells React Navigation how to handle the query parameters + parse: { + groupId: (groupId: string | number) => `${groupId}`, + 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 + // e.g., GroupDetails: "group/:id" + + }, - - // Other screens in your navigator can be defined here - // e.g., GroupDetails: "group/:id" }, - }, -}; + }; return ( @@ -98,6 +110,7 @@ const linking = { + ); diff --git a/src/redux/notificationSlice.ts b/src/redux/notificationSlice.ts index 8560a42..c500935 100644 --- a/src/redux/notificationSlice.ts +++ b/src/redux/notificationSlice.ts @@ -1,11 +1,25 @@ 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 export interface Notification { id: string; - title: string; - message: string; - time: string; + originalPriority: number; + priority: number; + sentTime: number; + data: NotificationData; + from: string; + messageId: string; + ttl: number; read: boolean; } @@ -23,28 +37,59 @@ const notificationSlice = createSlice({ name: 'notifications', initialState, reducers: { - addNotification: (state, action: PayloadAction) => { - // Adds the new notification to the top - state.notifications.unshift(action.payload); + addNotification: (state, action: PayloadAction>) => { + // Create a new notification with read status and add to the top + const newNotification: Notification = { + ...action.payload, + read: false, + }; + state.notifications.unshift(newNotification); }, + removeNotification: (state, action: PayloadAction) => { - // Remove notification by id + // Remove notification by id (using messageId as id) state.notifications = state.notifications.filter( - (item) => item.id !== action.payload + (item) => item.messageId !== action.payload ); }, + clearAllNotifications: (state) => { state.notifications = []; }, + markAsRead: (state, action: PayloadAction) => { - // Mark one notification as read + // Mark one notification as read by messageId const notification = state.notifications.find( - (item) => item.id === action.payload + (item) => item.messageId === action.payload ); if (notification) { 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, clearAllNotifications, markAsRead, + markAllAsRead, + removeExpiredNotifications, + updateNotificationPriority, } = notificationSlice.actions; -export default notificationSlice.reducer; +export default notificationSlice.reducer; \ No newline at end of file diff --git a/src/screens/ContributionScreen.tsx b/src/screens/ContributionScreen.tsx index ead24ba..0fdeaf5 100644 --- a/src/screens/ContributionScreen.tsx +++ b/src/screens/ContributionScreen.tsx @@ -56,7 +56,7 @@ const ContributionDetails: React.FC = ({ }, [visible]); return ( - + {/* Header */} = ({ { opacity: fadeAnim, transform: [{ translateY: slideAnim }] }, ]} > - + Contribution Summary @@ -126,7 +126,6 @@ const styles = StyleSheet.create({ headerTitle: { fontSize: 22, fontWeight: "700", - color: colors.primary, marginLeft: 10, }, card: { @@ -154,7 +153,7 @@ const styles = StyleSheet.create({ valueHighlight: { fontSize: 18, fontWeight: "700", - color: colors.primary, + color: colors.black, marginBottom: 10, }, }); diff --git a/src/screens/InviteScreen.tsx b/src/screens/InviteScreen.tsx index 0c062e4..5373457 100644 --- a/src/screens/InviteScreen.tsx +++ b/src/screens/InviteScreen.tsx @@ -77,7 +77,7 @@ const InviteScreen: FC = ({ route }) => { try { // Replace with your actual API call to join the group // 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}".`); } catch (e: any) { Alert.alert('Join Error', e.message || 'An unexpected error occurred.'); diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx index 3a44f1b..6690701 100644 --- a/src/screens/LoginScreen.tsx +++ b/src/screens/LoginScreen.tsx @@ -64,6 +64,7 @@ export default function LoginScreen() { if (mobile.length === 10) { setLoading(true); try { + console.log("Login api call:"); // FIX: Used the imported LOGIN_URL constant instead of a string literal const res = await axios.get(LOGIN_URL, { params: { mobile } }); console.log("Login response:", res.data); @@ -76,6 +77,7 @@ export default function LoginScreen() { [{ text: "Try Again", onPress: hideAlert }], 'error' ); + } } catch (error: any) { console.error("Login error:", error); diff --git a/src/screens/OtpVarificationScreen.tsx b/src/screens/OtpVarificationScreen.tsx index 249abef..d4580b4 100644 --- a/src/screens/OtpVarificationScreen.tsx +++ b/src/screens/OtpVarificationScreen.tsx @@ -20,7 +20,7 @@ import { LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls"; import colors from "../components/colors"; import LinearGradient from "react-native-linear-gradient"; import CustomAlert from "../components/CostomAlart"; -import FirebaseService from "../utils/firebase/firebaseServices"; +import { getFcmToken } from "../utils/firebase/firebaseServices"; const CELL_COUNT = 6; type OtpVerifyRouteProp = RouteProp; @@ -128,7 +128,7 @@ export default function OtpVerificationScreen() { const { jwt } = res.data?.data || {}; console.log("✅ OTP Verified:", res.data); await saveToken(jwt); - await FirebaseService.getFcmToken(res.data?.data?.userId); + await getFcmToken(res.data?.data?.userId); let hasActionRun = false; @@ -136,7 +136,7 @@ export default function OtpVerificationScreen() { if (hasActionRun) return; hasActionRun = true; hideAlert(); - navigation.navigate("Home"); + navigation.replace("Home"); }; triggerAlert( diff --git a/src/screens/home/GroupList.tsx b/src/screens/home/GroupList.tsx index 9bd3476..ece029d 100644 --- a/src/screens/home/GroupList.tsx +++ b/src/screens/home/GroupList.tsx @@ -110,7 +110,7 @@ const GroupList = () => { } }, [search, groups]); - if (loading) return ; + if (loading) return ; return ( <> @@ -175,10 +175,10 @@ const styles = StyleSheet.create({ alignItems: "center", 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 }, 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: { marginTop: 10, padding: 10, diff --git a/src/screens/home/Home.tsx b/src/screens/home/Home.tsx index 3514090..71e74ec 100644 --- a/src/screens/home/Home.tsx +++ b/src/screens/home/Home.tsx @@ -63,6 +63,9 @@ const userActions: ActionItem[] = [ const Home: React.FC = () => { const user = useSelector((state: any) => state.user); + const notifications = useSelector( + (state: any) => state.notifications.notifications + ); const navigation = useNavigation(); const dispatch = useDispatch(); @@ -98,6 +101,8 @@ const Home: React.FC = () => { } }; + const unreadCount = notifications.filter((item: any) => !item.read).length; + const renderGridItem: ListRenderItem = ({ item }) => ( { navigation.navigate("Notification" as never)}> {/* */} + {unreadCount > 0 ? unreadCount : ''} diff --git a/src/screens/home/RequestList.tsx b/src/screens/home/RequestList.tsx index 938d2b5..ca29730 100644 --- a/src/screens/home/RequestList.tsx +++ b/src/screens/home/RequestList.tsx @@ -125,7 +125,7 @@ const RequestList: React.FC = () => { // Make API call to approve the request 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", headers: { @@ -171,7 +171,7 @@ const RequestList: React.FC = () => { // Make API call to decline the request 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", headers: { diff --git a/src/screens/hook/useNotificationHandler.ts b/src/screens/hook/useNotificationHandler.ts new file mode 100644 index 0000000..50fc642 --- /dev/null +++ b/src/screens/hook/useNotificationHandler.ts @@ -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; + +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(); + 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 }; +}; \ No newline at end of file diff --git a/src/screens/management/EditGroupScreen.tsx b/src/screens/management/EditGroupScreen.tsx index 6708e00..11aa315 100644 --- a/src/screens/management/EditGroupScreen.tsx +++ b/src/screens/management/EditGroupScreen.tsx @@ -28,7 +28,7 @@ import *as Keychain from 'react-native-keychain' import Ionicons from '@react-native-vector-icons/ionicons'; import { CustomButton } from '../../components/common/Custom'; import { StackNavigationProp } from '@react-navigation/stack'; -import CustomModal from '../../components/common/CustomModal'; +// import CustomModal from '../../components/common/CustomModal'; import WinnerUser from './WinnerUser'; @@ -347,7 +347,7 @@ const EditGroupScreen: React.FC = ({ route }) => { {title} {isEditable && !editingCard && ( setEditingCard(cardKey)} style={styles.editButton}> - ✎ + Update )} @@ -538,7 +538,7 @@ const EditGroupScreen: React.FC = ({ route }) => { onPress={handleUpdateImage} icon={imageUpdating ? undefined : "cloud-upload"} style={{ paddingHorizontal: 20 }} - buttonBackground={colors.pastelBlue} + buttonBackground={colors.pastelOrange} /> @@ -675,7 +675,7 @@ const styles = StyleSheet.create({ position: "absolute", // right: 0, bottom: 0, - backgroundColor: colors.pastelBlue, + backgroundColor: colors.pastelOrange, borderRadius: 20, width: 40, height: 40, @@ -695,8 +695,8 @@ const styles = StyleSheet.create({ marginBottom: 12, }, cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937' }, - editButton: { padding: 8 }, - editButtonText: { color: colors.pastelPink, fontSize: 20, fontWeight: 'bold' }, + editButton: { padding: 8,backgroundColor:colors.pastelOrange, borderRadius: 15,elevation:5 }, + editButtonText: { color: colors.black, fontSize: 15, fontWeight: 'bold' }, fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingBottom: 12, marginBottom: 12 }, infoLabel: { fontSize: 16, color: '#6B7280', marginBottom: 4 }, 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' }, saveButton: { backgroundColor: colors.pastelOrange }, cancelButton: { backgroundColor: '#E5E7EB' }, - inviteButton: { backgroundColor: colors.primary }, - buttonText: { color: '#ffffffff', fontSize: 15, fontWeight: '600' }, + inviteButton: { backgroundColor: colors.pastelOrange }, + buttonText: { color: colors.black, fontSize: 15, fontWeight: '600' }, floatingButtonContainer: { position: 'absolute', bottom: 85, diff --git a/src/screens/management/ManagementScreen.tsx b/src/screens/management/ManagementScreen.tsx index e5634de..0bcad58 100644 --- a/src/screens/management/ManagementScreen.tsx +++ b/src/screens/management/ManagementScreen.tsx @@ -161,8 +161,8 @@ const ManagementScreen: FC = () => { const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f5f6fa' }, header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 }, - title: { fontSize: 22, fontWeight: "600", color: colors.Settings }, - segmentedControl: { flexDirection: 'row',borderRadius: 10, marginHorizontal: 20, marginBottom: 20, padding: 3 }, + title: { fontSize: 22, fontWeight: "600"}, + segmentedControl: { flexDirection: 'row',borderRadius: 10,marginBottom: 20, padding: 3 }, segmentButton: { flex: 1, paddingVertical: 12, alignItems: 'center', borderRadius: 8 }, segmentActive: { backgroundColor: colors.pastelOrange, elevation: 5,borderRadius:15, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 }, segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' }, diff --git a/src/screens/management/ManagementTabNavigation.tsx b/src/screens/management/ManagementTabNavigation.tsx index f961a8e..1703e63 100644 --- a/src/screens/management/ManagementTabNavigation.tsx +++ b/src/screens/management/ManagementTabNavigation.tsx @@ -1,22 +1,33 @@ import React from 'react'; -import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { View, Text, StyleSheet, Platform, TouchableOpacity } from 'react-native'; +import { createBottomTabNavigator, BottomTabBarProps } from '@react-navigation/bottom-tabs'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; 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 { 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 Member from './Member'; -type Props = NativeStackScreenProps; +// --- Type Definitions --- +type Props = NativeStackScreenProps; 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 insets = useSafeAreaInsets(); @@ -24,13 +35,10 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) = 0 ? insets.bottom : 16 }]}> {state.routes.map((route, index) => { const { options } = descriptors[route.key]; - const label = options.tabBarLabel ?? 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 label = options.tabBarLabel?.toString() ?? options.title ?? route.name; const isFocused = state.index === index; + + const iconName = iconMap[route.name] || 'help-circle-outline'; const onPress = () => { const event = navigation.emit({ @@ -60,15 +68,16 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) = testID={options.tabBarButtonTestID} onPress={onPress} onLongPress={onLongPress} - style={styles.tabItem} + // The style for the tab item itself no longer needs elevation + style={[styles.tabItem, { backgroundColor: isFocused ? colors.pastelOrange : 'transparent' }]} > - - {label as string} + + {label} ); @@ -77,69 +86,61 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) = ); }; + // --- Main Tab Navigator Component --- const ManagementTabNavigation: React.FC = ({ route }) => { - const { groupId } = route.params + const { groupId } = route.params; return ( } screenOptions={{ - headerShown: false, // Hides the default header for all screens in the tab + headerShown: false, }} > - {/* You can easily add more tabs here in the future */} ); }; + // --- Styles --- const styles = StyleSheet.create({ tabBarContainer: { flexDirection: 'row', position: 'absolute', - left: 30, - right: 30, + left: 20, // Adjusted for better visual balance + right: 20, // Adjusted for better visual balance backgroundColor: 'white', - borderRadius: 60, - paddingVertical: 4, + borderRadius: 30, + paddingVertical: 8, + paddingHorizontal: 8, // Added padding alignItems: 'center', justifyContent: 'space-around', - + // iOS Shadow shadowColor: '#000', shadowOffset: { width: 0, @@ -147,14 +148,24 @@ const styles = StyleSheet.create({ }, shadowOpacity: 0.1, shadowRadius: 10, + // Android Shadow elevation: 8, }, tabItem: { - flex: 1, + flex: 1, // Allow items to fill the space alignItems: '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: { fontSize: 12, marginTop: 4, diff --git a/src/screens/management/Member.tsx b/src/screens/management/Member.tsx index f0aff01..40e724b 100644 --- a/src/screens/management/Member.tsx +++ b/src/screens/management/Member.tsx @@ -128,12 +128,7 @@ const MemberScreen = ({ route }: Props) => { if (error) { return ( - - Oops! - {error} - - Try Again - + no members found ); } @@ -180,9 +175,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 20, paddingTop: 20, paddingBottom: 12, - backgroundColor: 'white', - borderBottomWidth: 1, - borderBottomColor: '#E5E7EB', + }, headerTitle: { fontSize: 28, diff --git a/src/screens/management/Payments.tsx b/src/screens/management/Payments.tsx index b259d3d..a241901 100644 --- a/src/screens/management/Payments.tsx +++ b/src/screens/management/Payments.tsx @@ -8,7 +8,7 @@ import { apiCall } from '../../utils/ApiHelper'; import { BASE_URL } from '../../utils/ApiUrls'; // --- 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 --- interface ApiResponse { @@ -43,54 +43,61 @@ interface PaymentDetails { type PaymentStackParamList = { Payments: { groupId: string } }; type PaymentsRouteProp = RouteProp; +// The component is a valid React Functional Component, which is required for using hooks. const Payments: React.FC = () => { + // --- HOOKS --- + // All hooks are called at the top level, which is correct. const route = useRoute(); const { groupId } = route.params; const [paymentDetails, setPaymentDetails] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); - - // State for the waiver modal + const [isGroupActive, setIsGroupActive] = useState(true); const [isModalVisible, setModalVisible] = useState(false); const [currentParticipant, setCurrentParticipant] = useState(null); const [waiverAmount, setWaiverAmount] = useState(''); const [cycleData, setCycleData] = useState(); + // --- FUNCTIONS --- + // These are regular functions and do not call hooks inside them, which is correct. const getCycleData = async () => { try { - - const response = await apiCall(`${API_BASE_URL}/groups/${groupId}/cycle-dashboard`, 'GET') as ApiResponse; + const response = await apiCall(`${API_BASE_URL}/groups/${groupId}/cycle-dashboard`, 'GET') as ApiResponse; if (response && response.statusCode === 200) { setCycleData(response.data); - } else { - Alert.alert("Error", response?.message || "Failed to fetch cycle data."); } } 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 () => { if (!refreshing) setLoading(true); + setIsGroupActive(true); + try { const response = await apiCall(`${BASE_URL}payments/current/payment/details/${groupId}`, 'GET') as ApiResponse; - console.log("payment response :", response) if (response && response.statusCode === 200) { setPaymentDetails(response.data); + } else if (response && response.statusCode === 400) { + setIsGroupActive(false); + setPaymentDetails(null); } else { - Alert.alert("Error", response?.message || "Failed to fetch details."); + setIsGroupActive(false); } } 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 { setLoading(false); setRefreshing(false); } }, [groupId, refreshing]); + // --- EFFECT HOOK --- + // useEffect is correctly used to call functions when the component mounts or dependencies change. useEffect(() => { getCycleData(); fetchPaymentDetails(); @@ -104,18 +111,17 @@ const Payments: React.FC = () => { const openWaiverDialog = (participant: Participant) => { setCurrentParticipant(participant); - setWaiverAmount(''); + setWaiverAmount(participant.lateFee.toString()); setModalVisible(true); }; - // --- UPDATED: handleWaiveFee now uses the correct API endpoint --- const handleWaiveFee = async () => { if (!currentParticipant || !paymentDetails) return; const amountToWaive = parseFloat(waiverAmount); - if (isNaN(amountToWaive) || amountToWaive <= 0) { - Alert.alert("Invalid Input", "Please enter a waiver amount greater than zero."); + if (isNaN(amountToWaive) || amountToWaive < 0) { + Alert.alert("Invalid Input", "Please enter a valid waiver amount."); return; } @@ -125,15 +131,13 @@ const Payments: React.FC = () => { } try { - // Construct the exact URL as provided const url = `${BASE_URL}groups/${groupId}/cycles/${paymentDetails.currentCycle}/waive/${currentParticipant.userId}?amountToWaive=${amountToWaive}`; - - const response = await apiCall(url, 'PUT', {}) as any; // Body is empty as amount is in URL + const response = await apiCall(url, 'PUT', {}) as any; if (response && response.statusCode === 200) { Alert.alert("Success", `₹${amountToWaive} has been waived successfully.`); setModalVisible(false); - onRefresh(); // Refresh data to reflect the change + onRefresh(); } else { throw new Error(response?.message || "Failed to apply waiver."); } @@ -142,13 +146,14 @@ const Payments: React.FC = () => { } }; + // --- RENDER LOGIC --- const renderParticipant = (participant: Participant) => { const isPending = participant.status === 'PENDING'; - const canWaive = isPending && participant.lateFee > 0; + const canWaive = isPending && participant.lateFee > 0 && !participant.lateFeeWaived; return ( - + {participant.firstName} {participant.lastName} {participant.mobile} @@ -163,7 +168,7 @@ const Payments: React.FC = () => { {isPending && participant.lateFee > 0 && ( Late Fee: ₹{participant.lateFee} )} - {participant.lateFee === 0 && participant.lateFeeWaived && Fee Waived} + {participant.lateFeeWaived && Fee Waived} @@ -210,36 +215,48 @@ const Payments: React.FC = () => { - } - > - {loading && !refreshing ? ( - - ) : ( - <> - - {paymentDetails?.groupName} - - - - Cycle - {paymentDetails?.currentCycle} + {loading && !refreshing ? ( + + ) : !isGroupActive ? ( + + + + This group is not currently active. + + + Tap to Refresh + + + ) : ( + } + > + {paymentDetails && ( + <> + + {paymentDetails.groupName} - - Contribution - ₹{paymentDetails?.contributionAmount} + + + Cycle + {paymentDetails.currentCycle} + + + Contribution + ₹{paymentDetails.contributionAmount} + + + Total Due + ₹{paymentDetails.totalDueWithLateFee} + - - Total Due - ₹{paymentDetails?.totalDueWithLateFee} - - - Participants - {paymentDetails?.participants.map(renderParticipant)} - - )} - + Participants + {paymentDetails.participants.map(renderParticipant)} + + )} + + )} ); }; @@ -251,7 +268,7 @@ const styles = StyleSheet.create({ summaryContainer: { flexDirection: 'row', justifyContent: 'space-around', padding: 20, backgroundColor: '#fff', marginHorizontal: 15, borderRadius: 12, elevation: 2 }, summaryBox: { alignItems: 'center' }, 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 }, participantCard: { backgroundColor: '#fff', marginHorizontal: 15, marginBottom: 10, borderRadius: 10, padding: 15, flexDirection: 'row', alignItems: 'center', elevation: 1 }, participantInfo: { flex: 1 }, @@ -274,6 +291,30 @@ const styles = StyleSheet.create({ cancelButtonText: { color: '#333', fontWeight: 'bold' }, saveButton: { backgroundColor: colors.primary }, 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; diff --git a/src/screens/notification/Notification.tsx b/src/screens/notification/Notification.tsx index 13127e8..6adcaff 100644 --- a/src/screens/notification/Notification.tsx +++ b/src/screens/notification/Notification.tsx @@ -8,86 +8,314 @@ import { } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; import { - addNotification, removeNotification, clearAllNotifications, + markAsRead, + markAllAsRead, } from '../../redux/notificationSlice'; import LinearGradient from 'react-native-linear-gradient'; import { Ionicons } from '@react-native-vector-icons/ionicons'; import AppView from '../../components/AppView'; 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 dispatch = useDispatch(); + const navigation = useNavigation(); + const { handleMarkAsReadAndNavigate } = useNotificationHandler(); const notifications = useSelector( (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) => { - dispatch(removeNotification(id)); + console.log("notifications ==>", notifications); + + const handleDelete = (messageId: string) => { + dispatch(removeNotification(messageId)); }; const handleClearAll = () => { 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 ( - {/* Header with Add and Clear All */} -
- - Delete All - - } - /> - - - {/* Add Notification Button */} - {/* - - Add Notification - - */} + {/* Header with actions */} +
0 ? `(${unreadCount})` : ''}`} /> + + {unreadCount > 0 && ( + + + Mark All Read + + + )} + {notifications.length > 0 && ( + + + Clear All + + + )} + {/* Notification List */} item.id} + keyExtractor={(item) => item.messageId} showsVerticalScrollIndicator={false} contentContainerStyle={styles.listContainer} renderItem={({ item }) => ( - - - - {item.title} - {item.message} - {item.time} + handleMarkAsRead(item.messageId, item?.data?.msgType)} + activeOpacity={0.7} + > + + + {/* Icon */} + + + + + {/* Content */} + + + + {item.data.title} + + + {formatTime(item.sentTime)} + + + + + {item.data.body} + + + {item.data.msgType && ( + + + {/* {item.data.msgType} */} + {getFilterDisplayName(item.data.msgType as FilterType)} + + + )} + + + {/* Actions */} + + {!item.read && ( + + )} + handleDelete(item.messageId)} + style={styles.deleteButton} + > + + + - handleDelete(item.id)}> - - - - + + )} ListEmptyComponent={ - No notifications found. + + + No notifications yet + + When you get notifications, they'll appear here + + } /> @@ -97,74 +325,130 @@ const Notification = () => { export default Notification; const styles = StyleSheet.create({ - headerContainer: { + headerActions: { flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', + gap: 8, + justifyContent: 'flex-end', paddingHorizontal: 16, - paddingTop: 12, + paddingVertical: 10, }, - headerTitle: { - fontSize: 20, - fontWeight: '700', - color: '#222', - }, - clearButtonGradient: { + headerButton: { borderRadius: 8, paddingVertical: 6, - paddingHorizontal: 14, - }, - clearButtonText: { - color: '#fff', - fontWeight: '600', - }, - addButtonGradient: { - margin: 16, - borderRadius: 8, - paddingVertical: 10, + paddingHorizontal: 12, + minWidth: 80, alignItems: 'center', }, - addButtonText: { + markReadButton: { + minWidth: 100, + }, + headerButtonText: { color: '#fff', fontWeight: '600', - fontSize: 16, + fontSize: 12, }, listContainer: { paddingHorizontal: 12, paddingBottom: 30, + paddingTop: 8, }, notificationCard: { - borderRadius: 10, - padding: 12, + borderRadius: 12, + padding: 16, marginBottom: 10, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5, - elevation: 2, + elevation: 3, + borderLeftWidth: 4, + borderLeftColor: 'transparent', + }, + unreadNotification: { + // borderLeftColor: colors.primary, + backgroundColor: '#fff', }, notificationContent: { 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: { - fontWeight: '700', + fontWeight: '600', fontSize: 16, color: '#333', + flex: 1, + marginRight: 8, + }, + unreadTitle: { + fontWeight: '700', + color: '#000', }, message: { fontSize: 14, - color: '#555', - marginTop: 2, + color: '#666', + lineHeight: 20, }, time: { fontSize: 12, - color: '#888', - marginTop: 4, + color: '#999', + 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: { - textAlign: 'center', - marginTop: 60, - fontSize: 16, - color: '#999', + fontSize: 18, + fontWeight: '600', + color: '#666', + marginTop: 16, + marginBottom: 8, }, -}); + emptySubtext: { + fontSize: 14, + color: '#999', + textAlign: 'center', + lineHeight: 20, + }, +}); \ No newline at end of file diff --git a/src/screens/profile/Profile.tsx b/src/screens/profile/Profile.tsx index 0ad60aa..cd8f078 100644 --- a/src/screens/profile/Profile.tsx +++ b/src/screens/profile/Profile.tsx @@ -29,7 +29,7 @@ import { clearUser, setUserData } from "../../redux/userSlice"; import { StackNavigationProp } from "@react-navigation/stack"; import { RootStackParamList } from "../../navigation/AppNavigator"; import { firebase } from "@react-native-firebase/messaging"; -import FirebaseService from "../../utils/firebase/firebaseServices"; +import FirebaseService, { deleteFcmToken } from "../../utils/firebase/firebaseServices"; type UserProfile = { @@ -157,13 +157,17 @@ const Profile = () => { const result = await response.json(); - if (result.status === "success") { - Alert.alert("Success", "Profile updated successfully"); - setEditing(false); - fetchUserProfile(); // reload updated data - } else { - Alert.alert("Error", "Failed to update profile"); - } + console.log("result for profile update :", result); + // Alert.alert("Success", "Profile updated successfully"); + // setEditing(false); + // fetchUserProfile(); + // if (result.status === "success") { + // Alert.alert("Success", "Profile updated successfully"); + // setEditing(false); + // fetchUserProfile(); // reload updated data + // } else { + // Alert.alert("Error", "Failed to update profile"); + // } } catch (error) { console.error("❌ Error updating profile:", error); // Alert.alert("Error", "Could not update profile"); @@ -305,7 +309,7 @@ const Profile = () => { return; } - await FirebaseService.deleteFcmToken(); + await deleteFcmToken(); // Clear stored credentials await Keychain.resetGenericPassword(); @@ -478,194 +482,196 @@ const Profile = () => { /> - - - - - Name - {user?.firstName} {user?.lastName} + + + + + Name + {user?.firstName} {user?.lastName} + + + + + + + + Email + {user?.email || "Not provided"} + + + + + + + + Mobile + {user?.mobile || "Not provided"} + + + + + + + + Usernames + {user?.username} + + + + + + + + Address + {user?.address || "Not provided"} + + + + + + + + Gender + {user?.gender || "Not specified"} + + + + + + + + Role + {user?.role || "User"} + - - - - - - Email - {user?.email || "Not provided"} - - - - - - - - Mobile - {user?.mobile || "Not provided"} - - - - - - - - Usernames - {user?.username} - - - - - - - - Address - {user?.address || "Not provided"} - - - - - - - - Gender - {user?.gender || "Not specified"} - - - - - - - - Role - {user?.role || "User"} - - - {/* BANK DETAILS SECTION */} Bank Information {/* Navigate to a screen you will create for editing bank details */} - + { navigation.navigate("EditBankDetails", { - currentBankName: user?.bankName, - currentAccountNumber: user?.bankAccountNumber, - currentBranchCode: user?.branchCode, - })}} + onPress={() => { + navigation.navigate("EditBankDetails", { + currentBankName: user?.bankName, + currentAccountNumber: user?.bankAccountNumber, + currentBranchCode: user?.branchCode, + }) + }} icon="create-outline" style={styles.editButton} - buttonBackground={colors.white} + buttonBackground={colors.white} /> - - - - - Bank Name - {user?.bankName || "Not provided"} + + + + + Bank Name + {user?.bankName || "Not provided"} + - - - - - Branch Code - {user?.branchCode || "Not provided"} + + + + Branch Code + {user?.branchCode || "Not provided"} + - - - - - Account Number - {user?.bankAccountNumber || "Not provided"} + + + + Account Number + {user?.bankAccountNumber || "Not provided"} + - ) : ( - <> - {/* Edit Form */} - - Edit Profile + <> + {/* Edit Form */} + + Edit Profile - setUser({ ...user, firstName: text })} - icon="person" - /> + setUser({ ...user, firstName: text })} + icon="person" + /> - setUser({ ...user, lastName: text })} - icon="person" - /> + setUser({ ...user, lastName: text })} + icon="person" + /> - setUser({ ...user, email: text })} - icon="mail" - keyboardType="email-address" - /> + setUser({ ...user, email: text })} + icon="mail" + keyboardType="email-address" + /> - setUser({ ...user, username: text })} - icon="at" - /> + setUser({ ...user, username: text })} + icon="at" + /> - setUser({ ...user, address: text })} - icon="location" - multiline - /> + setUser({ ...user, address: text })} + icon="location" + multiline + /> - setUser({ ...user, gender: text })} - icon="male-female" - /> + setUser({ ...user, gender: text })} + icon="male-female" + /> - - - setEditing(false)} - icon="close" - style={styles.cancelButton} - buttonBackground={colors.white} - /> - - - + + + setEditing(false)} + icon="close" + style={styles.cancelButton} + buttonBackground={colors.white} + /> + + + )} @@ -762,7 +768,7 @@ const styles = StyleSheet.create({ marginBottom: "40%", }, sectionTitle: { - fontSize: 18, + fontSize: 18, fontWeight: "700", color: "#1e293b", // marginBottom: 16, @@ -778,7 +784,7 @@ const styles = StyleSheet.create({ }, detailIcon: { marginRight: 20, - elevation:5 + elevation: 5 }, detailLabel: { fontSize: 12, @@ -791,12 +797,12 @@ const styles = StyleSheet.create({ fontWeight: "500", }, editButton: { - elevation:5, + elevation: 5, paddingHorizontal: 10, - paddingVertical:4, + paddingVertical: 4, borderRadius: 15, - borderWidth:2, - borderColor:colors.pastelBlue + borderWidth: 2, + borderColor: colors.pastelBlue }, editSection: { marginBottom: '100%', diff --git a/src/utils/ApiHelper.tsx b/src/utils/ApiHelper.tsx index 3306406..d16a1ab 100644 --- a/src/utils/ApiHelper.tsx +++ b/src/utils/ApiHelper.tsx @@ -36,32 +36,31 @@ export async function apiCall( }); // --- Response Logging --- - console.log(`--- API Response: [${response}] ${url} ---`); - // 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); + console.log(`--- API Response:${url}///// [${response}] `); return response.data; } catch (error: any) { if (error.response) { // Server responded with a status other than 2xx - console.error( - "API Error:", - `Status: ${error.response.status}`, - `Data: ${JSON.stringify(error.response.data)}` - ); + // console.error( + // "API Error:", + // `Status: ${error.response.status}`, + // `Data: ${JSON.stringify(error.response.data)}` + // ); if(error.response.status === 400){ + console.log("API Error:9090", url); ToastAndroid.show(error.response.data.message || responseErrorMessage, ToastAndroid.SHORT) } + } else if (error.request) { // 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 { // 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 throw error; } diff --git a/src/utils/ApiUrls.tsx b/src/utils/ApiUrls.tsx index 32433ac..bf4553b 100644 --- a/src/utils/ApiUrls.tsx +++ b/src/utils/ApiUrls.tsx @@ -1,6 +1,6 @@ -// 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 = "https://api.godemo.in/FundCollect/api/" +// export const BASE_URL = "http://192.168.1.5:9060/api/" export const LOGIN_URL = `${BASE_URL}user/generate/otp?mobiles=` export const VARIFY_OTP_URL = `${BASE_URL}user/otp/verify` export const ORGRAGISTRATION = `${BASE_URL}organizer/registration/apply` diff --git a/src/utils/firebase/firebaseServices.tsx b/src/utils/firebase/firebaseServices.tsx index e1a55b9..3cd4f95 100644 --- a/src/utils/firebase/firebaseServices.tsx +++ b/src/utils/firebase/firebaseServices.tsx @@ -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 notifee, { AndroidImportance } from '@notifee/react-native'; -import axios from 'axios'; import { apiCall } from '../ApiHelper'; 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 - static async requestPermission() { +// 🔹 Request notification permission +export const requestPermission = async () => { + try { await messaging().requestPermission(); await notifee.requestPermission(); + console.log('Notification permission granted'); + } catch (error) { + console.error('Error requesting notification permission:', error); } +}; - // 🔹 Display local notification - static async displayNotification(title: string, body: string) { +// 🔹 Display local notification +export const displayNotification = async (title: string, body: string, data?: any) => { + try { await notifee.displayNotification({ title, body, + data: data || {}, android: { channelId: 'default', - smallIcon: 'ic_launcher', // ensure you have this icon in android/app/src/main/res/mipmap + smallIcon: 'ic_launcher', importance: AndroidImportance.HIGH, pressAction: { id: 'default', }, }, }); + } catch (error) { + console.error('Error displaying notification:', error); } +}; - // 🔹 Get & send token to backend - static async getFcmToken(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; +// 🔹 Get & send token to backend +export const getFcmToken = async (userId: string) => { + console.log('Getting FCM Token...'); + try { + const token = await messaging().getToken(); + console.log('FCM Token:', token); + + // Send token to backend with user ID + await apiCall(STORE_FCM_TOKEN, 'POST', { + token, + userId + }); + console.log('FCM Token sent to backend', token); + + return token; + } catch (error) { + console.error('Error getting FCM token:', error); + return null; + } +}; + +// 🔹 Delete token from backend +export const deleteFcmToken = async (userId: string) => { + try { + const token = await messaging().getToken(); + console.log('FCM token to delete:', token); + + // Remove token from backend with user ID + await apiCall(REMOVE_FCM_TOKEN, 'POST', { + token, + userId + }); + console.log('FCM Token deleted from backend'); + + // Optional: Delete token from device + await messaging().deleteToken(); + } catch (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 +export const foregroundListener = () => { + return messaging().onMessage(async remoteMessage => { + console.log('Foreground message:', remoteMessage); + + if (!remoteMessage) return; + + 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 +export const backgroundHandler = () => { + // Handle background messages + messaging().setBackgroundMessageHandler(async remoteMessage => { + console.log('Background message:', remoteMessage); + + if (!remoteMessage) return; + + 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 => { + 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 => { + if (remoteMessage) { + console.log('App opened from quit state:', remoteMessage); + processAndAddNotification(remoteMessage); } - } + }); +}; - // 🔹 Delete token from backend - static async deleteFcmToken() { - 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); - } - } +// 🔹 Token auto-refresh +export const tokenRefreshHandler = () => { + return messaging().onTokenRefresh(async token => { + console.log('New refreshed FCM Token:', token); + // You can send the new token to your backend here if needed + // await apiCall(STORE_FCM_TOKEN, 'POST', { token }); + }); +}; - // 🔹 Foreground notifications - static foregroundListener() { - return messaging().onMessage(async remoteMessage => { - console.log('Foreground message:', remoteMessage); - if (remoteMessage.notification) { - await this.displayNotification( - remoteMessage.notification.title ?? 'New Message', - remoteMessage.notification.body ?? '' - ); - } - }); - } - - // 🔹 Background & Quit state - static backgroundHandler() { - messaging().setBackgroundMessageHandler(async remoteMessage => { - console.log('Background message:', remoteMessage); - if (remoteMessage.notification) { - await this.displayNotification( - remoteMessage.notification.title ?? 'New Message', - remoteMessage.notification.body ?? '' - ); - } - }); - - 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 - static tokenRefreshHandler() { - return messaging().onTokenRefresh(async token => { - console.log('New refreshed FCM Token:', token); - // 👉 send to backend - }); - } -} - -export default FirebaseService; +// Optional: Export all functions as a single object for easier imports +export default { + requestPermission, + displayNotification, + getFcmToken, + deleteFcmToken, + foregroundListener, + backgroundHandler, + tokenRefreshHandler, +}; \ No newline at end of file