handle notification type and navigate using deeplinking

This commit is contained in:
Jaanu423
2025-10-08 16:36:37 +05:30
25 changed files with 1228 additions and 530 deletions
+5 -5
View File
@@ -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({
+1 -1
View File
@@ -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>
+1
View File
@@ -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",
+8 -8
View File
@@ -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,
+7 -3
View File
@@ -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>
+16 -3
View File
@@ -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>
); );
+58 -10
View File
@@ -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;
+3 -4
View File
@@ -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,
}, },
}); });
+1 -1
View File
@@ -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.');
+2
View File
@@ -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);
+3 -3
View File
@@ -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(
+3 -3
View File
@@ -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,
+6
View File
@@ -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>
+2 -2
View File
@@ -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 };
};
+8 -8
View File
@@ -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,
+2 -2
View File
@@ -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,
+2 -9
View File
@@ -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,
+71 -30
View File
@@ -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;
+357 -73
View File
@@ -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,
}, },
}); });
+17 -11
View File
@@ -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
View File
@@ -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;
} }
+2 -2
View File
@@ -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`
+259 -42
View File
@@ -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,
};