notification
This commit is contained in:
@@ -5,23 +5,23 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { PaperProvider } from 'react-native-paper';
|
||||
import AppNavigator from './src/navigation/AppNavigator';
|
||||
import notifee from '@notifee/react-native';
|
||||
import FirebaseService from './src/utils/firebase/firebaseServices';
|
||||
import { backgroundHandler, foregroundListener, requestPermission, tokenRefreshHandler } from './src/utils/firebase/firebaseServices';
|
||||
|
||||
function App() {
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
|
||||
useEffect(() => {
|
||||
// 🔹 Ask for permission
|
||||
FirebaseService.requestPermission();
|
||||
requestPermission();
|
||||
|
||||
// 🔹 Handle background + quit state
|
||||
FirebaseService.backgroundHandler();
|
||||
backgroundHandler();
|
||||
|
||||
// 🔹 Listen for foreground messages
|
||||
const unsubscribe = FirebaseService.foregroundListener();
|
||||
const unsubscribe = foregroundListener();
|
||||
|
||||
// 🔹 Listen for token refresh
|
||||
const unsubscribeRefresh = FirebaseService.tokenRefreshHandler();
|
||||
const unsubscribeRefresh = tokenRefreshHandler();
|
||||
|
||||
// 🔹 Create default notification channel (Android)
|
||||
notifee.createChannel({
|
||||
|
||||
@@ -66,7 +66,7 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress })
|
||||
const navigation = useNavigation<any>()
|
||||
|
||||
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 */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.groupName}>{item.groupName}</Text>
|
||||
|
||||
@@ -94,6 +94,7 @@ const styles = StyleSheet.create({
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 4,
|
||||
marginHorizontal:12
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: "row",
|
||||
|
||||
@@ -37,8 +37,8 @@ export const RequestCard: FC<RequestCardProps> = ({ item, onApprove, onDecline }
|
||||
{/* Bottom section with action buttons */}
|
||||
<View style={styles.actionsSection}>
|
||||
<TouchableOpacity style={[styles.actionButton, styles.declineButton]} onPress={() => onDecline(item)}>
|
||||
<Ionicons name="close-outline" size={20} color={colors.pastelOrange} />
|
||||
<Text style={[styles.actionButtonText, { color: colors.pastelOrange }]}>Decline</Text>
|
||||
<Ionicons name="close-outline" size={20} color={colors.black} />
|
||||
<Text style={[styles.actionButtonText,]}>Decline</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.actionButton, styles.approveButton]} onPress={() => onApprove(item)}>
|
||||
{/* 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({
|
||||
card: {
|
||||
marginHorizontal: 12,
|
||||
backgroundColor: colors.white,
|
||||
borderRadius: 16,
|
||||
marginVertical: 8,
|
||||
elevation: 4,
|
||||
elevation: 5,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 10,
|
||||
@@ -67,9 +68,7 @@ const styles = StyleSheet.create({
|
||||
userInfoSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.textLight,
|
||||
paddingBottom: 12,
|
||||
paddingBottom: 6,
|
||||
},
|
||||
avatar: {
|
||||
width: 50,
|
||||
@@ -96,7 +95,6 @@ const styles = StyleSheet.create({
|
||||
requestText: {
|
||||
fontSize: 15,
|
||||
color: colors.textDark,
|
||||
lineHeight: 22,
|
||||
},
|
||||
groupName: {
|
||||
fontWeight: 'bold',
|
||||
@@ -116,11 +114,13 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
},
|
||||
declineButton: {
|
||||
backgroundColor: colors.pastelPink,
|
||||
elevation:5,
|
||||
backgroundColor:colors.white
|
||||
},
|
||||
approveButton: {
|
||||
// Set background color as requested
|
||||
backgroundColor: colors.pastelOrange,
|
||||
elevation:5,
|
||||
},
|
||||
actionButtonText: {
|
||||
fontSize: 16,
|
||||
|
||||
@@ -21,6 +21,8 @@ type CustomModalProps = {
|
||||
cancelText?: string;
|
||||
onConfirm?: () => void;
|
||||
confirmButton?:string
|
||||
confirmTextColor?:string
|
||||
elevation?:number
|
||||
};
|
||||
|
||||
const CustomModal: React.FC<CustomModalProps> = ({
|
||||
@@ -31,6 +33,8 @@ const CustomModal: React.FC<CustomModalProps> = ({
|
||||
confirmText = "Confirm",
|
||||
cancelText = "Cancel",
|
||||
onConfirm,
|
||||
confirmTextColor,
|
||||
elevation,
|
||||
confirmButton = colors.primary
|
||||
}) => {
|
||||
const slideAnim = useRef(new Animated.Value(height)).current; // starts off screen
|
||||
@@ -89,17 +93,17 @@ const CustomModal: React.FC<CustomModalProps> = ({
|
||||
{/* Footer Actions */}
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.cancelButton]}
|
||||
style={[styles.button, styles.cancelButton,{elevation:elevation}]}
|
||||
onPress={onClose}
|
||||
>
|
||||
<Text style={styles.cancelText}>{cancelText}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, {backgroundColor:confirmButton}]}
|
||||
style={[styles.button, {backgroundColor:confirmButton ,elevation:elevation}]}
|
||||
onPress={onConfirm}
|
||||
>
|
||||
<Text style={styles.confirmText}>{confirmText}</Text>
|
||||
<Text style={[styles.confirmText,{color:confirmTextColor}]}>{confirmText}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
@@ -56,7 +56,7 @@ const ContributionDetails: React.FC<ContributionProps> = ({
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<CustomModal visible={visible} onClose={onClose} onConfirm={onConfirm}>
|
||||
<CustomModal visible={visible} onClose={onClose} onConfirm={onConfirm} confirmButton={colors.pastelOrange} confirmTextColor={colors.black} elevation={5}>
|
||||
{/* Header */}
|
||||
<Animated.View
|
||||
style={[
|
||||
@@ -64,7 +64,7 @@ const ContributionDetails: React.FC<ContributionProps> = ({
|
||||
{ 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>
|
||||
</Animated.View>
|
||||
|
||||
@@ -126,7 +126,6 @@ const styles = StyleSheet.create({
|
||||
headerTitle: {
|
||||
fontSize: 22,
|
||||
fontWeight: "700",
|
||||
color: colors.primary,
|
||||
marginLeft: 10,
|
||||
},
|
||||
card: {
|
||||
@@ -154,7 +153,7 @@ const styles = StyleSheet.create({
|
||||
valueHighlight: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
color: colors.primary,
|
||||
color: colors.black,
|
||||
marginBottom: 10,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await apiCall(`https://api.godemo.in/FundCollect/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
|
||||
const response = await apiCall(`http://192.168.1.5:9060/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
|
||||
if (response.status === 'success' && response.data) {
|
||||
setGroupDetails(response.data);
|
||||
} else {
|
||||
@@ -76,7 +76,7 @@ const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
|
||||
try {
|
||||
// Replace with your actual API call to join the group
|
||||
// For now, it just shows an alert.
|
||||
await apiCall(`https://api.godemo.in/FundCollect/api/fund/groups/join/invite/${code}`, 'POST');
|
||||
await apiCall(`http://192.168.1.5:9060/api/fund/groups/join/invite/${code}`, 'POST');
|
||||
Alert.alert('Success!', `You have requested to join "${groupDetails.groupName}".`);
|
||||
} catch (e: any) {
|
||||
Alert.alert('Join Error', e.message || 'An unexpected error occurred.');
|
||||
|
||||
@@ -64,6 +64,7 @@ export default function LoginScreen() {
|
||||
if (mobile.length === 10) {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log("Login api call:");
|
||||
// FIX: Used the imported LOGIN_URL constant instead of a string literal
|
||||
const res = await axios.get(LOGIN_URL, { params: { mobile } });
|
||||
console.log("Login response:", res.data);
|
||||
@@ -76,6 +77,7 @@ export default function LoginScreen() {
|
||||
[{ text: "Try Again", onPress: hideAlert }],
|
||||
'error'
|
||||
);
|
||||
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Login error:", error);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls";
|
||||
import colors from "../components/colors";
|
||||
import LinearGradient from "react-native-linear-gradient";
|
||||
import CustomAlert from "../components/CostomAlart";
|
||||
import FirebaseService from "../utils/firebase/firebaseServices";
|
||||
import { getFcmToken } from "../utils/firebase/firebaseServices";
|
||||
|
||||
const CELL_COUNT = 6;
|
||||
type OtpVerifyRouteProp = RouteProp<RootStackParamList, "OtpVarify">;
|
||||
@@ -128,7 +128,7 @@ export default function OtpVerificationScreen() {
|
||||
const { jwt } = res.data?.data || {};
|
||||
console.log("✅ OTP Verified:", res.data);
|
||||
await saveToken(jwt);
|
||||
await FirebaseService.getFcmToken(res.data?.data?.userId);
|
||||
await getFcmToken(res.data?.data?.userId);
|
||||
|
||||
|
||||
let hasActionRun = false;
|
||||
|
||||
@@ -110,7 +110,7 @@ const GroupList = () => {
|
||||
}
|
||||
}, [search, groups]);
|
||||
|
||||
if (loading) return <ActivityIndicator size="large" color={colors.Home} />;
|
||||
if (loading) return <ActivityIndicator size="large" color={colors.pastelBlue} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -175,10 +175,10 @@ const styles = StyleSheet.create({
|
||||
alignItems: "center",
|
||||
marginRight: 12,
|
||||
},
|
||||
avatarText: { fontSize: 18, fontWeight: "700", color: colors.Home },
|
||||
avatarText: { fontSize: 18, fontWeight: "700", color: colors.black },
|
||||
groupName: { fontSize: 16, fontWeight: "600", color: colors.textDark },
|
||||
groupDescription: { fontSize: 13, color: colors.textLight, marginTop: 2 },
|
||||
groupMeta: { fontSize: 12, color: colors.Home, marginTop: 2 },
|
||||
groupMeta: { fontSize: 12, color: colors.black, marginTop: 2 },
|
||||
detailsBox: {
|
||||
marginTop: 10,
|
||||
padding: 10,
|
||||
|
||||
@@ -58,7 +58,7 @@ const RequestList: React.FC = () => {
|
||||
setError(null);
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.godemo.in/FundCollect/api/fund/group/pending/request",
|
||||
"http://192.168.1.5:9060/api/fund/group/pending/request",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -124,7 +124,7 @@ const RequestList: React.FC = () => {
|
||||
|
||||
// Make API call to approve the request
|
||||
const response = await fetch(
|
||||
`https://api.godemo.in/FundCollect/api/fund/requests/${requestId}/approve`,
|
||||
`http://192.168.1.5:9060/api/fund/requests/${requestId}/approve`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
@@ -170,7 +170,7 @@ const RequestList: React.FC = () => {
|
||||
|
||||
// Make API call to decline the request
|
||||
const response = await fetch(
|
||||
`https://api.godemo.in/FundCollect/api/fund/requests/${requestedId}/reject?reason=/${reason}`,
|
||||
`http://192.168.1.5:9060/api/fund/requests/${requestedId}/reject?reason=/${reason}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
|
||||
@@ -28,7 +28,7 @@ import *as Keychain from 'react-native-keychain'
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
import { CustomButton } from '../../components/common/Custom';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import CustomModal from '../../components/common/CustomModal';
|
||||
// import CustomModal from '../../components/common/CustomModal';
|
||||
import WinnerUser from './WinnerUser';
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
<Text style={styles.cardTitle}>{title}</Text>
|
||||
{isEditable && !editingCard && (
|
||||
<TouchableOpacity onPress={() => setEditingCard(cardKey)} style={styles.editButton}>
|
||||
<Text style={styles.editButtonText}>✎</Text>
|
||||
<Text style={styles.editButtonText}>Update</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
@@ -537,7 +537,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
onPress={handleUpdateImage}
|
||||
icon={imageUpdating ? undefined : "cloud-upload"}
|
||||
style={{ paddingHorizontal: 20 }}
|
||||
buttonBackground={colors.pastelBlue}
|
||||
buttonBackground={colors.pastelOrange}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -674,7 +674,7 @@ const styles = StyleSheet.create({
|
||||
position: "absolute",
|
||||
// right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: colors.pastelBlue,
|
||||
backgroundColor: colors.pastelOrange,
|
||||
borderRadius: 20,
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -694,8 +694,8 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 12,
|
||||
},
|
||||
cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937' },
|
||||
editButton: { padding: 8 },
|
||||
editButtonText: { color: colors.pastelPink, fontSize: 20, fontWeight: 'bold' },
|
||||
editButton: { padding: 8,backgroundColor:colors.pastelOrange, borderRadius: 15,elevation:5 },
|
||||
editButtonText: { color: colors.black, fontSize: 15, fontWeight: 'bold' },
|
||||
fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingBottom: 12, marginBottom: 12 },
|
||||
infoLabel: { fontSize: 16, color: '#6B7280', marginBottom: 4 },
|
||||
infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
|
||||
@@ -712,8 +712,8 @@ const styles = StyleSheet.create({
|
||||
button: { borderRadius: 10, paddingVertical: 12, paddingHorizontal: 20, marginLeft: 10, minWidth: 80, alignItems: 'center', justifyContent: 'center' },
|
||||
saveButton: { backgroundColor: colors.pastelOrange },
|
||||
cancelButton: { backgroundColor: '#E5E7EB' },
|
||||
inviteButton: { backgroundColor: colors.primary },
|
||||
buttonText: { color: '#ffffffff', fontSize: 15, fontWeight: '600' },
|
||||
inviteButton: { backgroundColor: colors.pastelOrange },
|
||||
buttonText: { color: colors.black, fontSize: 15, fontWeight: '600' },
|
||||
floatingButtonContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 85,
|
||||
|
||||
@@ -161,8 +161,8 @@ const ManagementScreen: FC = () => {
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#f5f6fa' },
|
||||
header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 },
|
||||
title: { fontSize: 22, fontWeight: "600", color: colors.Settings },
|
||||
segmentedControl: { flexDirection: 'row',borderRadius: 10, marginHorizontal: 20, marginBottom: 20, padding: 3 },
|
||||
title: { fontSize: 22, fontWeight: "600"},
|
||||
segmentedControl: { flexDirection: 'row',borderRadius: 10,marginBottom: 20, padding: 3 },
|
||||
segmentButton: { flex: 1, paddingVertical: 12, alignItems: 'center', borderRadius: 8 },
|
||||
segmentActive: { backgroundColor: colors.pastelOrange, elevation: 5,borderRadius:15, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
|
||||
segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' },
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
import React from 'react';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { View, Text, StyleSheet, Platform, TouchableOpacity } from 'react-native';
|
||||
import { createBottomTabNavigator, BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import {Ionicons} from '@react-native-vector-icons/ionicons'; // Correct import for react-native-vector-icons
|
||||
|
||||
import { Ionicons } from '@react-native-vector-icons/ionicons';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
|
||||
import EditGroupScreen from './EditGroupScreen'; // Your screen component
|
||||
import colors from '../../components/colors';
|
||||
|
||||
|
||||
// --- Screen Imports ---
|
||||
import EditGroupScreen from './EditGroupScreen';
|
||||
import Payments from './Payments';
|
||||
import Member from './Member';
|
||||
|
||||
type Props = NativeStackScreenProps<RootStackParamList, 'ManagementTabs'>;
|
||||
|
||||
// --- Type Definitions ---
|
||||
type Props = NativeStackScreenProps<RootStackParamList, 'ManagementTabs'>;
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
// --- Custom Floating Tab Bar Component ---
|
||||
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
||||
|
||||
// --- Icon Mapping ---
|
||||
const iconMap: { [key: string]: string } = {
|
||||
EditGroup: 'trail-sign-outline',
|
||||
Payments: 'trending-up',
|
||||
Member: 'people-outline',
|
||||
};
|
||||
|
||||
|
||||
// --- Custom Floating Tab Bar Component ---
|
||||
const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
@@ -24,14 +35,11 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
|
||||
<View style={[styles.tabBarContainer, { bottom: insets.bottom > 0 ? insets.bottom : 16 }]}>
|
||||
{state.routes.map((route, index) => {
|
||||
const { options } = descriptors[route.key];
|
||||
const label = options.tabBarLabel ?? options.title ?? route.name;
|
||||
|
||||
console.log("option : ", options)
|
||||
// A custom option to pass the icon name
|
||||
const iconName = label === "Settings" ? "trail-sign-outline" : label === "Payments" ? "trending-up" : label === "Members" ? "people-outline" : "flower";
|
||||
|
||||
const label = options.tabBarLabel?.toString() ?? options.title ?? route.name;
|
||||
const isFocused = state.index === index;
|
||||
|
||||
const iconName = iconMap[route.name] || 'help-circle-outline';
|
||||
|
||||
const onPress = () => {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
@@ -60,15 +68,16 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
|
||||
testID={options.tabBarButtonTestID}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
style={styles.tabItem}
|
||||
// The style for the tab item itself no longer needs elevation
|
||||
style={[styles.tabItem, { backgroundColor: isFocused ? colors.pastelOrange : 'transparent' }]}
|
||||
>
|
||||
<Ionicons
|
||||
name={iconName}
|
||||
size={24}
|
||||
color={isFocused ? '#4F46E5' : '#6B7280'}
|
||||
color={isFocused ? colors.black : '#6B7280'}
|
||||
/>
|
||||
<Text style={[styles.tabLabel, { color: isFocused ? '#4F46E5' : '#6B7280' }]}>
|
||||
{label as string}
|
||||
<Text style={[styles.tabLabel, { color: isFocused ? colors.black : '#6B7280' }]}>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -77,69 +86,61 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// --- Main Tab Navigator Component ---
|
||||
const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
|
||||
const { groupId } = route.params
|
||||
const { groupId } = route.params;
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
// Assign the custom floating tab bar
|
||||
tabBar={props => <FloatingTabBar {...props} />}
|
||||
screenOptions={{
|
||||
headerShown: false, // Hides the default header for all screens in the tab
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="EditGroup"
|
||||
component={EditGroupScreen}
|
||||
// Pass the groupId to the EditGroupScreen as an initial parameter
|
||||
initialParams={{ groupId }}
|
||||
options={{
|
||||
tabBarLabel: 'Settings',
|
||||
// Custom prop to define the icon for our FloatingTabBar
|
||||
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Payments"
|
||||
component={Payments}
|
||||
// Pass the groupId to the EditGroupScreen as an initial parameter
|
||||
initialParams={{ groupId }}
|
||||
options={{
|
||||
tabBarLabel: 'Payments',
|
||||
|
||||
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Member"
|
||||
component={Member}
|
||||
// Pass the groupId to the EditGroupScreen as an initial parameter
|
||||
initialParams={{ groupId }}
|
||||
options={{
|
||||
tabBarLabel: 'Members',
|
||||
|
||||
}}
|
||||
|
||||
/>
|
||||
{/* You can easily add more tabs here in the future */}
|
||||
</Tab.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// --- Styles ---
|
||||
const styles = StyleSheet.create({
|
||||
tabBarContainer: {
|
||||
flexDirection: 'row',
|
||||
position: 'absolute',
|
||||
left: 30,
|
||||
right: 30,
|
||||
left: 20, // Adjusted for better visual balance
|
||||
right: 20, // Adjusted for better visual balance
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 60,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 30,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 8, // Added padding
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
|
||||
// iOS Shadow
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
@@ -147,14 +148,24 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 10,
|
||||
// Android Shadow
|
||||
elevation: 8,
|
||||
},
|
||||
tabItem: {
|
||||
flex: 1,
|
||||
flex: 1, // Allow items to fill the space
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingVertical: 10, // Increased padding for a better touch area
|
||||
borderRadius: 25,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 10, // This is for the background color highlight
|
||||
},
|
||||
// The 'activeTab' style is no longer needed as the logic is handled inline
|
||||
tabLabel: {
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
|
||||
@@ -56,7 +56,7 @@ const MemberScreen = ({ route }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiCall<ApiResponse>(
|
||||
`https://api.godemo.in/FundCollect/api/fund/group/all/member?groupId=${groupId}`,
|
||||
`http://192.168.1.5:9060/api/fund/group/all/member?groupId=${groupId}`,
|
||||
'GET'
|
||||
);
|
||||
|
||||
@@ -118,12 +118,7 @@ const MemberScreen = ({ route }) => {
|
||||
if (error) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<Ionicons name="cloud-offline-outline" size={60} color="#EF4444" />
|
||||
<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>
|
||||
<Text style={styles.errorText}>no members found</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -170,9 +165,7 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 12,
|
||||
backgroundColor: 'white',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E5E7EB',
|
||||
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 28,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { apiCall } from '../../utils/ApiHelper';
|
||||
import { BASE_URL } from '../../utils/ApiUrls';
|
||||
|
||||
// --- CONSTANTS ---
|
||||
const API_BASE_URL = 'https://api.godemo.in/FundCollect/api/payments';
|
||||
const API_BASE_URL = 'http://192.168.1.5:9060/api/payments';
|
||||
|
||||
// --- TYPE DEFINITIONS ---
|
||||
interface ApiResponse<T> {
|
||||
@@ -43,54 +43,61 @@ interface PaymentDetails {
|
||||
type PaymentStackParamList = { Payments: { groupId: string } };
|
||||
type PaymentsRouteProp = RouteProp<PaymentStackParamList, 'Payments'>;
|
||||
|
||||
// The component is a valid React Functional Component, which is required for using hooks.
|
||||
const Payments: React.FC = () => {
|
||||
// --- HOOKS ---
|
||||
// All hooks are called at the top level, which is correct.
|
||||
const route = useRoute<PaymentsRouteProp>();
|
||||
const { groupId } = route.params;
|
||||
|
||||
const [paymentDetails, setPaymentDetails] = useState<PaymentDetails | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// State for the waiver modal
|
||||
const [isGroupActive, setIsGroupActive] = useState(true);
|
||||
const [isModalVisible, setModalVisible] = useState(false);
|
||||
const [currentParticipant, setCurrentParticipant] = useState<Participant | null>(null);
|
||||
const [waiverAmount, setWaiverAmount] = useState('');
|
||||
const [cycleData, setCycleData] = useState<any>();
|
||||
|
||||
// --- FUNCTIONS ---
|
||||
// These are regular functions and do not call hooks inside them, which is correct.
|
||||
const getCycleData = async () => {
|
||||
try {
|
||||
|
||||
const response = await apiCall(`${API_BASE_URL}/groups/${groupId}/cycle-dashboard`, 'GET') as ApiResponse<any>;
|
||||
if (response && response.statusCode === 200) {
|
||||
setCycleData(response.data);
|
||||
} else {
|
||||
Alert.alert("Error", response?.message || "Failed to fetch cycle data.");
|
||||
}
|
||||
} catch (error: any) {
|
||||
Alert.alert("Error", error.message || "An unexpected error occurred.");
|
||||
console.error("Failed to fetch cycle data:", error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPaymentDetails = useCallback(async () => {
|
||||
if (!refreshing) setLoading(true);
|
||||
setIsGroupActive(true);
|
||||
|
||||
try {
|
||||
const response = await apiCall(`${BASE_URL}payments/current/payment/details/${groupId}`, 'GET') as ApiResponse<PaymentDetails>;
|
||||
|
||||
console.log("payment response :", response)
|
||||
if (response && response.statusCode === 200) {
|
||||
setPaymentDetails(response.data);
|
||||
} else if (response && response.statusCode === 400) {
|
||||
setIsGroupActive(false);
|
||||
setPaymentDetails(null);
|
||||
} else {
|
||||
Alert.alert("Error", response?.message || "Failed to fetch details.");
|
||||
setIsGroupActive(false);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Alert.alert("Error", error.message || "An unexpected error occurred.");
|
||||
console.log("payment error : ", error)
|
||||
console.log("payment error: ", error);
|
||||
setIsGroupActive(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [groupId, refreshing]);
|
||||
|
||||
// --- EFFECT HOOK ---
|
||||
// useEffect is correctly used to call functions when the component mounts or dependencies change.
|
||||
useEffect(() => {
|
||||
getCycleData();
|
||||
fetchPaymentDetails();
|
||||
@@ -104,18 +111,17 @@ const Payments: React.FC = () => {
|
||||
|
||||
const openWaiverDialog = (participant: Participant) => {
|
||||
setCurrentParticipant(participant);
|
||||
setWaiverAmount('');
|
||||
setWaiverAmount(participant.lateFee.toString());
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
// --- UPDATED: handleWaiveFee now uses the correct API endpoint ---
|
||||
const handleWaiveFee = async () => {
|
||||
if (!currentParticipant || !paymentDetails) return;
|
||||
|
||||
const amountToWaive = parseFloat(waiverAmount);
|
||||
|
||||
if (isNaN(amountToWaive) || amountToWaive <= 0) {
|
||||
Alert.alert("Invalid Input", "Please enter a waiver amount greater than zero.");
|
||||
if (isNaN(amountToWaive) || amountToWaive < 0) {
|
||||
Alert.alert("Invalid Input", "Please enter a valid waiver amount.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -125,15 +131,13 @@ const Payments: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Construct the exact URL as provided
|
||||
const url = `${BASE_URL}groups/${groupId}/cycles/${paymentDetails.currentCycle}/waive/${currentParticipant.userId}?amountToWaive=${amountToWaive}`;
|
||||
|
||||
const response = await apiCall(url, 'PUT', {}) as any; // Body is empty as amount is in URL
|
||||
const response = await apiCall(url, 'PUT', {}) as any;
|
||||
|
||||
if (response && response.statusCode === 200) {
|
||||
Alert.alert("Success", `₹${amountToWaive} has been waived successfully.`);
|
||||
setModalVisible(false);
|
||||
onRefresh(); // Refresh data to reflect the change
|
||||
onRefresh();
|
||||
} else {
|
||||
throw new Error(response?.message || "Failed to apply waiver.");
|
||||
}
|
||||
@@ -142,9 +146,10 @@ const Payments: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- RENDER LOGIC ---
|
||||
const renderParticipant = (participant: Participant) => {
|
||||
const isPending = participant.status === 'PENDING';
|
||||
const canWaive = isPending && participant.lateFee > 0;
|
||||
const canWaive = isPending && participant.lateFee > 0 && !participant.lateFeeWaived;
|
||||
|
||||
return (
|
||||
<View key={participant.userId} style={styles.participantCard}>
|
||||
@@ -163,7 +168,7 @@ const Payments: React.FC = () => {
|
||||
{isPending && participant.lateFee > 0 && (
|
||||
<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 style={styles.menuContainer}>
|
||||
@@ -210,36 +215,48 @@ const Payments: React.FC = () => {
|
||||
</View>
|
||||
</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
|
||||
style={styles.container}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||
>
|
||||
{loading && !refreshing ? (
|
||||
<ActivityIndicator style={{marginTop: 50}} size="large" color={colors.primary} />
|
||||
) : (
|
||||
{paymentDetails && (
|
||||
<>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>{paymentDetails?.groupName}</Text>
|
||||
<Text style={styles.headerTitle}>{paymentDetails.groupName}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryContainer}>
|
||||
<View style={styles.summaryBox}>
|
||||
<Text style={styles.summaryLabel}>Cycle</Text>
|
||||
<Text style={styles.summaryValue}>{paymentDetails?.currentCycle}</Text>
|
||||
<Text style={styles.summaryValue}>{paymentDetails.currentCycle}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryBox}>
|
||||
<Text style={styles.summaryLabel}>Contribution</Text>
|
||||
<Text style={styles.summaryValue}>₹{paymentDetails?.contributionAmount}</Text>
|
||||
<Text style={styles.summaryValue}>₹{paymentDetails.contributionAmount}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryBox}>
|
||||
<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>
|
||||
<Text style={styles.sectionTitle}>Participants</Text>
|
||||
{paymentDetails?.participants.map(renderParticipant)}
|
||||
{paymentDetails.participants.map(renderParticipant)}
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</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 },
|
||||
summaryBox: { alignItems: 'center' },
|
||||
summaryLabel: { fontSize: 14, color: '#6c757d', marginBottom: 4 },
|
||||
summaryValue: { fontSize: 18, fontWeight: '700', color: colors.primary },
|
||||
summaryValue: { fontSize: 18, fontWeight: '700', color: colors.black },
|
||||
sectionTitle: { fontSize: 20, fontWeight: 'bold', marginLeft: 20, marginTop: 20, marginBottom: 10 },
|
||||
participantCard: { backgroundColor: '#fff', marginHorizontal: 15, marginBottom: 10, borderRadius: 10, padding: 15, flexDirection: 'row', alignItems: 'center', elevation: 1 },
|
||||
participantInfo: { flex: 1 },
|
||||
@@ -274,6 +291,30 @@ const styles = StyleSheet.create({
|
||||
cancelButtonText: { color: '#333', fontWeight: 'bold' },
|
||||
saveButton: { backgroundColor: colors.primary },
|
||||
saveButtonText: { color: 'white', fontWeight: 'bold' },
|
||||
fullScreenMessage: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#f4f6f8',
|
||||
padding: 20,
|
||||
},
|
||||
fullScreenMessageText: {
|
||||
fontSize: 18,
|
||||
color: '#333',
|
||||
textAlign: 'center',
|
||||
marginTop: 15,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
refreshButton: {
|
||||
marginTop: 20,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 8,
|
||||
},
|
||||
refreshButtonText: {
|
||||
color: colors.black,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
export default Payments;
|
||||
|
||||
@@ -43,7 +43,7 @@ const BecomeOrg = () => {
|
||||
const token = credentials.password;
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.godemo.in/FundCollect/api/organizer/registration/apply",
|
||||
"http://192.168.1.5:9060/api/organizer/registration/apply",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -29,7 +29,7 @@ import { clearUser, setUserData } from "../../redux/userSlice";
|
||||
import { StackNavigationProp } from "@react-navigation/stack";
|
||||
import { RootStackParamList } from "../../navigation/AppNavigator";
|
||||
import { firebase } from "@react-native-firebase/messaging";
|
||||
import FirebaseService from "../../utils/firebase/firebaseServices";
|
||||
import FirebaseService, { deleteFcmToken } from "../../utils/firebase/firebaseServices";
|
||||
|
||||
|
||||
type UserProfile = {
|
||||
@@ -305,7 +305,7 @@ const Profile = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await FirebaseService.deleteFcmToken();
|
||||
await deleteFcmToken();
|
||||
|
||||
// Clear stored credentials
|
||||
await Keychain.resetGenericPassword();
|
||||
@@ -377,7 +377,7 @@ const Profile = () => {
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>User Profile</Text>
|
||||
<TouchableOpacity onPress={() => confirmLogout(navigation)} style={styles.refreshButton}>
|
||||
<Ionicons name="log-out-outline" size={24} color={colors.Profile} />
|
||||
<Ionicons name="log-out-outline" size={24} color={colors.black} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{/* if (fetching || !user) {
|
||||
@@ -701,7 +701,7 @@ const styles = StyleSheet.create({
|
||||
borderBottomRightRadius: 20,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 22, fontWeight: "600", color: colors.Profile
|
||||
fontSize: 22, fontWeight: "600", color: colors.black
|
||||
},
|
||||
refreshButton: {
|
||||
padding: 4,
|
||||
|
||||
+12
-13
@@ -36,32 +36,31 @@ export async function apiCall<T>(
|
||||
});
|
||||
|
||||
// --- Response Logging ---
|
||||
console.log(`--- API Response: [${response}] ${url} ---`);
|
||||
// Uncomment the line below to see the full response data in logs
|
||||
// console.log("Response Data:", JSON.stringify(response.data, null, 2));
|
||||
console.log("---------------------------------------",response);
|
||||
console.log(`--- API Response:${url}///// [${response}] `);
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
// Server responded with a status other than 2xx
|
||||
console.error(
|
||||
"API Error:",
|
||||
`Status: ${error.response.status}`,
|
||||
`Data: ${JSON.stringify(error.response.data)}`
|
||||
);
|
||||
// console.error(
|
||||
// "API Error:",
|
||||
// `Status: ${error.response.status}`,
|
||||
// `Data: ${JSON.stringify(error.response.data)}`
|
||||
// );
|
||||
if(error.response.status === 400){
|
||||
console.log("API Error:9090", url);
|
||||
ToastAndroid.show(error.response.data.message || responseErrorMessage, ToastAndroid.SHORT)
|
||||
}
|
||||
|
||||
} else if (error.request) {
|
||||
// Request was made but no response received
|
||||
console.error("API Error: No response received", error.request);
|
||||
// console.error("API Error: No response received", error.request);
|
||||
} else {
|
||||
// Something else went wrong while setting up the request
|
||||
console.error("API Error:", error);
|
||||
// console.error("API Error:", error);
|
||||
}
|
||||
console.error("-----------------");
|
||||
|
||||
console.log(`API : ${url} API Error:`, error);
|
||||
return error.response.status
|
||||
// Re-throw the error so that the calling function can handle it if needed
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -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 VARIFY_OTP_URL = `${BASE_URL}user/otp/verify`
|
||||
export const ORGRAGISTRATION = `${BASE_URL}organizer/registration/apply`
|
||||
|
||||
@@ -4,17 +4,14 @@ import axios from 'axios';
|
||||
import { apiCall } from '../ApiHelper';
|
||||
import { REMOVE_FCM_TOKEN, STORE_FCM_TOKEN } from '../ApiUrls';
|
||||
|
||||
// replace with your backend
|
||||
|
||||
class FirebaseService {
|
||||
// 🔹 Request notification permission
|
||||
static async requestPermission() {
|
||||
export const requestPermission = async () => {
|
||||
await messaging().requestPermission();
|
||||
await notifee.requestPermission();
|
||||
}
|
||||
};
|
||||
|
||||
// 🔹 Display local notification
|
||||
static async displayNotification(title: string, body: string) {
|
||||
export const displayNotification = async (title: string, body: string) => {
|
||||
await notifee.displayNotification({
|
||||
title,
|
||||
body,
|
||||
@@ -27,10 +24,10 @@ class FirebaseService {
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 🔹 Get & send token to backend
|
||||
static async getFcmToken(userId: string) {
|
||||
export const getFcmToken = async (userId: string) => {
|
||||
console.log('FCM Token o');
|
||||
try {
|
||||
console.log('FCM Token 1');
|
||||
@@ -44,43 +41,49 @@ class FirebaseService {
|
||||
console.error('Error getting FCM token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 🔹 Delete token from backend
|
||||
static async deleteFcmToken() {
|
||||
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)
|
||||
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
|
||||
static foregroundListener() {
|
||||
export const foregroundListener = () => {
|
||||
return messaging().onMessage(async remoteMessage => {
|
||||
console.log('Foreground message:', remoteMessage);
|
||||
if (remoteMessage.notification) {
|
||||
await this.displayNotification(
|
||||
remoteMessage.notification.title ?? 'New Message',
|
||||
remoteMessage.notification.body ?? ''
|
||||
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
|
||||
static backgroundHandler() {
|
||||
export const backgroundHandler = () => {
|
||||
messaging().setBackgroundMessageHandler(async remoteMessage => {
|
||||
console.log('Background message:', remoteMessage);
|
||||
if (remoteMessage.notification) {
|
||||
await this.displayNotification(
|
||||
remoteMessage.notification.title ?? 'New Message',
|
||||
remoteMessage.notification.body ?? ''
|
||||
const { messageId, notification, data } = remoteMessage;
|
||||
if (!remoteMessage) return;
|
||||
const { title, body } = data || {};
|
||||
if (remoteMessage) {
|
||||
await displayNotification(
|
||||
(title as string) ?? 'New Message',
|
||||
(body as string) ?? ''
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -89,20 +92,30 @@ class FirebaseService {
|
||||
console.log('App opened from background:', remoteMessage);
|
||||
});
|
||||
|
||||
messaging().getInitialNotification().then(remoteMessage => {
|
||||
messaging()
|
||||
.getInitialNotification()
|
||||
.then(remoteMessage => {
|
||||
if (remoteMessage) {
|
||||
console.log('App opened from quit state:', remoteMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 🔹 Token auto-refresh
|
||||
static tokenRefreshHandler() {
|
||||
export const tokenRefreshHandler = () => {
|
||||
return messaging().onTokenRefresh(async token => {
|
||||
console.log('New refreshed FCM Token:', token);
|
||||
// 👉 send to backend
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default FirebaseService;
|
||||
// Optional: Export all functions as a single object for easier imports
|
||||
export default {
|
||||
requestPermission,
|
||||
displayNotification,
|
||||
getFcmToken,
|
||||
deleteFcmToken,
|
||||
foregroundListener,
|
||||
backgroundHandler,
|
||||
tokenRefreshHandler,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user