feat: Implement new Notification, PaymentMode, and Profile screens, alongside Android manifest and package updates.

This commit is contained in:
Abin Toppo
2026-03-27 10:37:19 +05:30
parent 5ba1c5d3f0
commit 84d609d4ac
6 changed files with 431 additions and 240 deletions
+14 -13
View File
@@ -20,7 +20,7 @@ import { ThemeProvider } from './src/theme/ThemeProvider';
LogBox.ignoreAllLogs(true);
const queryClient = new QueryClient({
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
@@ -29,8 +29,9 @@ const queryClient = new QueryClient({
},
});
function App() {
import { GestureHandlerRootView } from 'react-native-gesture-handler';
function App() {
useEffect(() => {
requestPermission();
@@ -53,18 +54,18 @@ function App() {
};
}, []);
return (
<SafeAreaProvider>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<PaperProvider >
<AppNavigator />
</PaperProvider>
</ThemeProvider>
</QueryClientProvider>
</SafeAreaProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<PaperProvider >
<AppNavigator />
</PaperProvider>
</ThemeProvider>
</QueryClientProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
+1
View File
@@ -19,6 +19,7 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.MAIN" />
+1
View File
@@ -41,6 +41,7 @@
"react-native-dialog": "^9.3.0",
"react-native-elements": "^3.4.3",
"react-native-fast-confetti": "^1.1.1",
"react-native-gesture-handler": "^2.28.0",
"react-native-image-picker": "^8.2.1",
"react-native-inset-shadow": "^1.0.3",
"react-native-keychain": "^10.0.0",
+341 -205
View File
@@ -1,14 +1,16 @@
import React from 'react';
import React, { useState } from 'react';
import {
View,
Text,
TouchableOpacity,
FlatList,
StyleSheet,
Alert,
Dimensions,
} from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { removeNotification } from '../../redux/notificationSlice';
import { removeNotification, clearAllNotifications, markAllAsRead } from '../../redux/notificationSlice';
import LinearGradient from 'react-native-linear-gradient';
import LucideIcon from '../../components/common/LucideIcon';
@@ -16,13 +18,18 @@ import AppView from '../../components/AppView';
import Header from '../../components/common/Header';
import { useNotificationHandler } from '../hook/useNotificationHandler';
import { useTheme } from '../../theme/ThemeProvider';
import Swipeable from 'react-native-gesture-handler/ReanimatedSwipeable';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
interpolate,
Extrapolate,
SharedValue,
} from 'react-native-reanimated';
import FontsFamily from '../../constents/FontsFamily';
const { width } = Dimensions.get('window');
type FilterType =
| 'all'
@@ -52,6 +59,28 @@ const Notification = () => {
dispatch(removeNotification(messageId));
};
const handleClearAll = () => {
if (notifications.length === 0) return;
Alert.alert(
'Clear All',
'Are you sure you want to delete all notifications?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Clear All',
style: 'destructive',
onPress: () => dispatch(clearAllNotifications())
},
]
);
};
const handleMarkAllRead = () => {
if (notifications.length === 0) return;
dispatch(markAllAsRead());
};
const handleMarkAsRead = (
messageId: string,
screen: string,
@@ -72,36 +101,47 @@ const Notification = () => {
const formatTime = (timestamp: number) => {
const date = new Date(timestamp);
const now = new Date();
const diff = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
const diff = (now.getTime() - date.getTime()) / (1000 * 60);
if (diff < 1) return 'Just now';
if (diff < 24) return `${Math.floor(diff)}h ago`;
return date.toLocaleDateString();
if (diff < 60) return `${Math.floor(diff)}m ago`;
if (diff < 1440) return `${Math.floor(diff / 60)}h ago`;
const dayDiff = Math.floor(diff / 1440);
if (dayDiff === 1) return 'Yesterday';
if (dayDiff < 7) return `${dayDiff}d ago`;
return date.toLocaleDateString('en-IN', { day: '2-digit', month: 'short' });
};
const getNotificationIcon = (type: string) => {
const map: any = {
JOIN_REQUEST: 'person-add-outline',
CONTRIBUTION: 'cash-outline',
CONTRIBUTION_ACK: 'checkmark-done-outline',
CYCLE_COMPLETED: 'trophy-outline',
WITHDRAWAL: 'card-outline',
CONTRIBUTION_ACK: 'checkmark-circle-outline',
CYCLE_COMPLETED: 'trophy',
WITHDRAWAL: 'wallet-outline',
WITHDRAWAL_NOTIFY: 'notifications-outline',
GROUP_STARTED: 'rocket-outline',
GROUP_STARTED: 'sparkles-outline',
JOIN_APPROVED: 'checkmark-circle-outline',
JOIN_VIA_INVITE: 'mail-outline',
};
return map[type] || 'notifications-outline';
};
const getNotificationColor = (type: string) => {
const map: any = {
JOIN_REQUEST: '#FFB6C1',
CONTRIBUTION: '#98FB98',
CONTRIBUTION_ACK: '#ADD8E6',
CYCLE_COMPLETED: '#D8BFD8',
WITHDRAWAL: '#FFD700',
WITHDRAWAL_NOTIFY: '#FFA07A',
JOIN_REQUEST: '#FF6B6B', // Vibrant Red
CONTRIBUTION: '#4ECDC4', // Modern Teal
CONTRIBUTION_ACK: '#45B7D1', // Sky Blue
CYCLE_COMPLETED: '#FFD93D', // Golden Yellow
WITHDRAWAL: '#6C5CE7', // Deep Purple
WITHDRAWAL_NOTIFY: '#FF8066', // Coral
GROUP_STARTED: '#A8E6CF', // Mint Green
JOIN_APPROVED: '#32D74B', // iOS Green
JOIN_VIA_INVITE: '#007AFF', // iOS Blue
};
return map[type] || '#D3D3D3';
return map[type] || '#8E8E93';
};
const getFilterDisplayName = (filter: FilterType) => {
@@ -110,135 +150,162 @@ const Notification = () => {
const unreadCount = notifications.filter((i: any) => !i.read).length;
// 🔥 Swipe Delete UI
const renderRightActions = (
messageId: string,
onDeleteAction: (id: string) => void,
swipeableMethods?: { close?: () => void }
swipeableMethods: { close: () => void },
drag: SharedValue<number>
) => {
const styleAnimation = useAnimatedStyle(() => {
const translateX = interpolate(
drag.value,
[-100, 0],
[0, 100],
Extrapolate.CLAMP
);
return {
transform: [{ translateX }],
};
});
return (
<TouchableOpacity
onPress={() => {
onDeleteAction(messageId);
swipeableMethods?.close?.();
swipeableMethods.close();
}}
style={styles(theme).deleteSwipe}
activeOpacity={0.8}
style={styles(theme).deleteSwipeContainer}
>
<LucideIcon name="trash-outline" size={22} color="#fff" />
<Text style={styles(theme).deleteText}>Delete</Text>
<Animated.View style={[styles(theme).deleteSwipe, styleAnimation]}>
<LucideIcon name="trash-outline" size={24} color="#fff" />
<Text style={styles(theme).deleteText}>Delete</Text>
</Animated.View>
</TouchableOpacity>
);
};
const NotificationItem = ({
item,
onDelete,
onPressNotification,
theme,
}: {
item: any;
onDelete: (id: string) => void;
onPressNotification: (
messageId: string,
screen: string,
groupId: string,
cycleNumber: string,
contributionId: string
) => void;
theme: any;
}) => {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const NotificationItem = ({
item,
onDelete,
onPressNotification,
theme,
}: {
item: any;
onDelete: (id: string) => void;
onPressNotification: (
messageId: string,
screen: string,
groupId: string,
cycleNumber: string,
contributionId: string
) => void;
theme: any;
}) => {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<Swipeable
renderRightActions={(_, __, swipeableMethods) =>
renderRightActions(item.messageId, onDelete, swipeableMethods)
}
>
<Animated.View style={animatedStyle}>
<TouchableOpacity
activeOpacity={0.9}
onPressIn={() => (scale.value = withSpring(0.96))}
onPressOut={() => (scale.value = withSpring(1))}
onPress={() =>
onPressNotification(
item.messageId,
item?.data?.msgType,
item?.data?.groupId,
item?.data?.cycleNumber,
item?.data?.contributionId
)
}
>
<LinearGradient
colors={
item.read
? [theme.colors.white, '#F8FAFF']
: ['#FFF7ED', '#EEF2FF']
return (
<Swipeable
friction={2}
enableTrackpadTwoFingerGesture
rightThreshold={40}
renderRightActions={(prog, drag, swipeableMethods) =>
renderRightActions(item.messageId, onDelete, swipeableMethods, drag)
}
>
<Animated.View style={animatedStyle}>
<TouchableOpacity
activeOpacity={0.9}
onPressIn={() => (scale.value = withSpring(0.98))}
onPressOut={() => (scale.value = withSpring(1))}
onPress={() =>
onPressNotification(
item.messageId,
item?.data?.msgType,
item?.data?.groupId,
item?.data?.cycleNumber,
item?.data?.contributionId
)
}
style={[
styles(theme).card,
!item.read && styles(theme).unread,
{
borderLeftColor: getNotificationColor(
item.data.msgType
),
},
]}
>
<View style={styles(theme).row}>
<View style={styles(theme).iconBox}>
<LucideIcon
name={getNotificationIcon(item.data.msgType)}
size={22}
color={getNotificationColor(item.data.msgType)}
/>
</View>
<View style={styles(theme).content}>
<View style={styles(theme).topRow}>
<Text
style={[
styles(theme).title,
!item.read && styles(theme).unreadTitle,
]}
>
{item.data.title}
</Text>
<Text style={styles(theme).time}>
{formatTime(item.sentTime)}
</Text>
<View
style={[
styles(theme).card,
!item.read && styles(theme).unreadCard,
]}
>
<View style={styles(theme).cardContent}>
<View
style={[
styles(theme).iconContainer,
{ backgroundColor: getNotificationColor(item.data.msgType) + '15' }
]}
>
<LucideIcon
name={getNotificationIcon(item.data.msgType)}
size={24}
color={getNotificationColor(item.data.msgType)}
/>
</View>
<Text style={styles(theme).message}>{item.data.body}</Text>
<View style={styles(theme).textContent}>
<View style={styles(theme).headerRow}>
<Text
numberOfLines={1}
style={[
styles(theme).title,
!item.read && styles(theme).unreadTitle
]}
>
{item.data.title}
</Text>
<Text style={styles(theme).time}>
{formatTime(item.sentTime)}
</Text>
</View>
<View style={styles(theme).tagWrap}>
<Text
style={[
styles(theme).tag,
{
backgroundColor: getNotificationColor(
item.data.msgType
),
},
]}
>
{getFilterDisplayName(item.data.msgType)}
<Text numberOfLines={2} style={styles(theme).bodyText}>
{item.data.body}
</Text>
<View style={styles(theme).footerRow}>
<View
style={[
styles(theme).badge,
{ backgroundColor: getNotificationColor(item.data.msgType) + '20' }
]}
>
<View
style={[
styles(theme).badgeDot,
{ backgroundColor: getNotificationColor(item.data.msgType) }
]}
/>
<Text
style={[
styles(theme).badgeText,
{ color: getNotificationColor(item.data.msgType) }
]}
>
{getFilterDisplayName(item.data.msgType)}
</Text>
</View>
{!item.read && (
<View style={styles(theme).unreadIndicator} />
)}
</View>
</View>
</View>
{!item.read && <View style={styles(theme).dot} />}
</View>
</LinearGradient>
</TouchableOpacity>
</Animated.View>
</Swipeable>
);
};
</TouchableOpacity>
</Animated.View>
</Swipeable>
);
};
const renderItem = ({ item }: any) => (
<NotificationItem
@@ -252,9 +319,28 @@ const NotificationItem = ({
return (
<AppView>
<Header
title={
'Notifications' +
(unreadCount ? ` (${unreadCount})` : '')
title="Notifications"
rightAction={
<View style={styles(theme).headerActions}>
{notifications.length > 0 && (
<TouchableOpacity
onPress={handleMarkAllRead}
style={styles(theme).headerIconButton}
accessibilityLabel="Mark all as read"
>
<LucideIcon name="checkmark-circle-outline" size={20} color={theme.colors.primary} />
</TouchableOpacity>
)}
{notifications.length > 0 && (
<TouchableOpacity
onPress={handleClearAll}
style={styles(theme).headerIconButton}
accessibilityLabel="Clear all notifications"
>
<LucideIcon name="trash-outline" size={20} color="#FF3B30" />
</TouchableOpacity>
)}
</View>
}
/>
@@ -263,16 +349,19 @@ const NotificationItem = ({
keyExtractor={(item) => item.messageId}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles(theme).container}
contentContainerStyle={styles(theme).listContainer}
ListEmptyComponent={
<View style={styles(theme).empty}>
<LucideIcon
name="notifications-off-outline"
size={64}
color="#ccc"
/>
<Text style={styles(theme).emptyText}>
No notifications
<View style={styles(theme).emptyContainer}>
<View style={styles(theme).emptyCircle}>
<LucideIcon
name="notifications-off-outline"
size={48}
color="#A1A1AA"
/>
</View>
<Text style={styles(theme).emptyTitle}>Stay Tuned!</Text>
<Text style={styles(theme).emptySubText}>
We'll notify you when something important happens.
</Text>
</View>
}
@@ -285,117 +374,164 @@ export default Notification;
const styles = (theme: any) =>
StyleSheet.create({
container: {
padding: 12,
listContainer: {
padding: 16,
paddingBottom: 40,
},
card: {
borderRadius: 16,
padding: 16,
backgroundColor: theme.colors.white || '#FFFFFF',
borderRadius: 20,
padding: 14,
marginBottom: 12,
backgroundColor: 'rgba(255,255,255,0.95)',
shadowColor: '#000',
shadowOpacity: 0.08,
shadowRadius: 10,
elevation: 4,
borderLeftWidth: 5,
},
unread: {
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 2,
borderWidth: 1,
borderColor: '#E0E7FF',
shadowColor: '#6366F1',
shadowOpacity: 0.15,
borderColor: '#F4F4F5',
},
row: {
unreadCard: {
borderColor: (theme.colors.primary || '#6366F1') + '20',
backgroundColor: (theme.colors.primary || '#6366F1') + '05',
},
cardContent: {
flexDirection: 'row',
gap: 12,
alignItems: 'flex-start',
},
iconBox: {
backgroundColor: '#F1F5F9',
padding: 10,
borderRadius: 12,
iconContainer: {
width: 48,
height: 48,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
content: {
textContent: {
flex: 1,
},
topRow: {
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
title: {
fontWeight: '700',
fontFamily: FontsFamily.nunitoBold,
fontSize: 15,
color: theme.colors.textPrimary,
color: theme.colors.textPrimary || '#18181B',
flex: 1,
marginRight: 8,
},
unreadTitle: {
color: theme.colors.primary,
color: theme.colors.primary || '#6366F1',
},
message: {
fontSize: 13,
color: theme.colors.textSecondary,
marginTop: 4,
},
time: {
fontFamily: FontsFamily.nunitoRegular,
fontSize: 11,
color: '#6B7280',
color: '#71717A',
},
tagWrap: {
marginTop: 6,
bodyText: {
fontFamily: FontsFamily.nunitoRegular,
fontSize: 13,
color: theme.colors.textSecondary || '#52525B',
lineHeight: 18,
marginBottom: 8,
},
tag: {
fontSize: 10,
color: '#fff',
paddingHorizontal: 10,
footerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
badge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 20,
fontWeight: '700',
alignSelf: 'flex-start',
borderRadius: 8,
},
dot: {
badgeDot: {
width: 6,
height: 6,
borderRadius: 3,
marginRight: 6,
},
badgeText: {
fontFamily: FontsFamily.nunitoSemiBold,
fontSize: 10,
textTransform: 'capitalize',
},
unreadIndicator: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: theme.colors.primary,
marginTop: 6,
backgroundColor: theme.colors.primary || '#6366F1',
},
deleteSwipeContainer: {
width: 90,
height: '100%',
justifyContent: 'center',
alignItems: 'flex-end',
marginBottom: 12, // Match card margin
},
deleteSwipe: {
backgroundColor: '#FF3B30',
width: 80,
height: '84%', // A bit shorter than the container for visual balance
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
width: 80,
borderRadius: 16,
marginBottom: 12,
shadowColor: '#FF3B30',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 5,
},
deleteText: {
color: '#fff',
color: '#FFFFFF',
fontFamily: FontsFamily.nunitoBold,
fontSize: 12,
marginTop: 4,
},
empty: {
headerActions: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 120,
gap: 8,
},
emptyText: {
marginTop: 12,
color: '#999',
fontSize: 16,
headerIconButton: {
width: 36,
height: 36,
borderRadius: 10,
backgroundColor: '#F4F4F5',
justifyContent: 'center',
alignItems: 'center',
},
});
emptyContainer: {
alignItems: 'center',
justifyContent: 'center',
marginTop: width * 0.4,
paddingHorizontal: 40,
},
emptyCircle: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: '#F4F4F5',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 24,
},
emptyTitle: {
fontFamily: FontsFamily.nunitoBold,
fontSize: 20,
color: theme.colors.textPrimary,
marginBottom: 8,
},
emptySubText: {
fontFamily: FontsFamily.nunitoRegular,
fontSize: 14,
color: '#71717A',
textAlign: 'center',
lineHeight: 20,
},
});
+47 -20
View File
@@ -319,9 +319,9 @@ const PaymentModeScreen: React.FC = () => {
`${BASE_URL}payments/pg/entry-fee/${groupId}?groupId=${groupId}&paymentId=${selectedMode}&mobileNumber=`,
'POST',
payload
) as any;
) as any;
console.log("payment res",res)
console.log("payment res", res)
if (res?.status === 'success' || res?.statusCode === 200) {
const pgOrderNo = res?.data?.pgOrderNo;
@@ -341,7 +341,7 @@ const PaymentModeScreen: React.FC = () => {
setPaying(false);
}
}
const handlePaymentForRouteFormGroupDetails = async () => {
const handlePaymentForRouteFormGroupDetails = async (PaymentMode: number) => {
try {
setPaying(true);
@@ -358,36 +358,63 @@ const PaymentModeScreen: React.FC = () => {
'POST',
payload
) as any;
console.log("payment payload ",payload)
console.log("payment res ",res)
if (res?.status === 'success' || res?.statusCode === 200) {
const pgOrderNo = res?.data?.pgOrderNo;
if (pgOrderNo) {
// Start polling for transaction status
pollTransactionStatus(pgOrderNo);
} else {
ToastAndroid.show('Payment initiated but no order number received', ToastAndroid.SHORT);
setPaying(false);
}
} else {
ToastAndroid.show(res?.data?.message || res?.message || 'Payment failed', ToastAndroid.SHORT);
setPaying(false);
console.log("payment payload ", payload)
console.log("payment res ", res)
switch (PaymentMode) {
case 6:
{
if (res?.status === 'success' || res?.statusCode === 200) {
const pgOrderNo = res?.data?.pgOrderNo;
if (pgOrderNo) {
// Start polling for transaction status
pollTransactionStatus(pgOrderNo);
} else {
ToastAndroid.show('Payment initiated but no order number received', ToastAndroid.SHORT);
setPaying(false);
}
} else {
ToastAndroid.show(res?.data?.message || res?.message || 'Payment failed', ToastAndroid.SHORT);
setPaying(false);
}
}
break;
default:
{
if (res?.status === 'success' || res?.statusCode === 200) {
const pgOrderNo = res?.data?.pgOrderNo;
if (pgOrderNo) {
// Start polling for transaction status
pollTransactionStatus(pgOrderNo);
} else {
ToastAndroid.show('Payment initiated but no order number received', ToastAndroid.SHORT);
setPaying(false);
}
} else {
ToastAndroid.show(res?.data?.message || res?.message || 'Payment failed', ToastAndroid.SHORT);
setPaying(false);
}
}
break;
}
} catch (error) {
ToastAndroid.show('An error occurred during payment', ToastAndroid.SHORT);
setPaying(false);
}
}
const handleConfirmPayment = async () => {
console.log("selectedMode", selectedMode)
if (!selectedMode) {
ToastAndroid.show('Please select a payment method', ToastAndroid.SHORT);
return;
}
switch (routeFrom) {
case 'GroupDetails':
handlePaymentForRouteFormGroupDetails();
handlePaymentForRouteFormGroupDetails(selectedMode);
break;
case 'GroupScreen':
handlePaymentForRouteFormGroupScreen();
+27 -2
View File
@@ -39,6 +39,9 @@ import {
USER_MOBILE_MONEY_URL,
} from "../../utils/ApiUrls";
import SwitchNavigator from "./SwitchNavigator";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { QueryClient } from "@tanstack/react-query";
import { queryClient } from "../../../App";
interface UserProfile {
id: string;
@@ -183,9 +186,12 @@ const Profile = () => {
refetchStatus();
}, [refetchProfile, refetchStatus]);
const handleLogout = async () => {
setIsLoggingOut(true);
try {
// 🔹 Remove FCM Token
const fcmToken = await getFcmToken(user?.userId?.toString());
if (fcmToken) {
await fetch(`${BASE_URL}token/remove?fcmToken=${fcmToken}`, {
@@ -194,15 +200,34 @@ const Profile = () => {
Authorization: `Bearer ${(await Keychain.getGenericPassword() as any).password}`,
},
});
await deleteFcmToken(user?.userId?.toString());
}
// 🔹 Clear Redux State
dispatch(clearAllNotifications());
await Keychain.resetGenericPassword();
dispatch(clearUser());
navigation.replace("Login");
// 🔹 Clear AsyncStorage (IMPORTANT 🚨)
await AsyncStorage.clear();
// 🔹 Clear Keychain (secure storage)
await Keychain.resetGenericPassword();
// 🔹 Clear React Query / API Cache (if using)
if (queryClient) {
queryClient.clear();
}
// 🔹 Reset Navigation (avoid back navigation)
navigation.reset({
index: 0,
routes: [{ name: 'Login' }],
});
} catch (error) {
console.error("Logout error:", error);
} finally {
setIsLoggingOut(false);
}
};