666 lines
19 KiB
TypeScript
666 lines
19 KiB
TypeScript
import React from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
FlatList,
|
|
} from "react-native";
|
|
import {
|
|
User,
|
|
ChevronRight,
|
|
ArrowUpRight,
|
|
ArrowDownLeft,
|
|
UploadCloud,
|
|
DollarSign,
|
|
Ticket,
|
|
} from "lucide-react-native";
|
|
import BackButton from "~/components/ui/backButton";
|
|
import { useNotifications } from "~/lib/hooks/useNotifications";
|
|
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
|
|
import { NotificationService } from "~/lib/services/notificationService";
|
|
import { RequestService } from "~/lib/services/requestService";
|
|
import { router } from "expo-router";
|
|
import { ROUTES } from "~/lib/routes";
|
|
import ScreenWrapper from "~/components/ui/ScreenWrapper";
|
|
import ModalToast from "~/components/ui/toast";
|
|
import { useTranslation } from "react-i18next";
|
|
import PermissionAlertModal from "~/components/ui/permissionAlertModal";
|
|
|
|
// Notification Card Component
|
|
const NotificationCard = ({
|
|
notification,
|
|
onPress,
|
|
onMoneyRequestAction,
|
|
onMoneyRequestPrompt,
|
|
}: {
|
|
notification: any;
|
|
onPress?: () => void;
|
|
onMoneyRequestAction?: (
|
|
requestId: string,
|
|
action: "accept" | "decline"
|
|
) => void;
|
|
onMoneyRequestPrompt?: (notification: any) => void;
|
|
}) => {
|
|
console.log("Notification:", notification);
|
|
const getNotificationIcon = () => {
|
|
const rawType = (notification.type || "").toString().toLowerCase();
|
|
const title = (notification.title || "").toString().toLowerCase();
|
|
const message = (notification.message || "").toString().toLowerCase();
|
|
const transactionSubtype = (
|
|
notification.transactionType ||
|
|
notification.transaction_type ||
|
|
notification.subtype ||
|
|
""
|
|
)
|
|
.toString()
|
|
.toLowerCase();
|
|
|
|
// Event notifications (tickets, event reminders, etc.)
|
|
if (
|
|
rawType === "event" ||
|
|
rawType === "event_ticket_purchased" ||
|
|
rawType === "event_reminder" ||
|
|
notification.category === "event"
|
|
) {
|
|
return <Ticket color="#FFFFFF" size={20} />;
|
|
}
|
|
|
|
// Money request notifications
|
|
if (
|
|
rawType === "request_received" ||
|
|
title.includes("request") ||
|
|
message.includes("request")
|
|
) {
|
|
// Arrow going 45° down for money request
|
|
return <ArrowDownLeft color="#FFFFFF" size={20} />;
|
|
}
|
|
|
|
// Cash out (money icon, red theme handled by color helper)
|
|
if (
|
|
rawType === "cash_out" ||
|
|
title.includes("cash out") ||
|
|
message.includes("cash out")
|
|
) {
|
|
return <DollarSign color="#FFFFFF" size={20} />;
|
|
}
|
|
|
|
// Money received (same money icon but green theme)
|
|
if (
|
|
rawType === "money_received" ||
|
|
rawType === "receive" ||
|
|
title.includes("received") ||
|
|
message.includes("received")
|
|
) {
|
|
return <DollarSign color="#FFFFFF" size={20} />;
|
|
}
|
|
|
|
// Money sent (arrow 45° up)
|
|
if (
|
|
rawType === "money_sent" ||
|
|
rawType === "send" ||
|
|
title.includes("sent") ||
|
|
message.includes("sent")
|
|
) {
|
|
return <ArrowUpRight color="#FFFFFF" size={20} />;
|
|
}
|
|
|
|
// Fallback for generic transaction-related notifications
|
|
if (
|
|
rawType === "transaction_completed" ||
|
|
rawType === "transaction" ||
|
|
title.includes("transaction") ||
|
|
message.includes("transaction")
|
|
) {
|
|
return <DollarSign color="#FFFFFF" size={20} />;
|
|
}
|
|
|
|
// Everything else fall back to user icon
|
|
return <User color="#FFFFFF" size={20} />;
|
|
};
|
|
|
|
const getNotificationColor = () => {
|
|
const rawType = (notification.type || "").toString().toLowerCase();
|
|
const title = (notification.title || "").toString().toLowerCase();
|
|
const message = (notification.message || "").toString().toLowerCase();
|
|
const transactionSubtype = (
|
|
notification.transactionType ||
|
|
notification.transaction_type ||
|
|
notification.subtype ||
|
|
""
|
|
)
|
|
.toString()
|
|
.toLowerCase();
|
|
|
|
// --- Explicit transaction subtypes (preferred) ---
|
|
if (transactionSubtype === "cash_out") {
|
|
// Cash out → red
|
|
return "bg-red-50";
|
|
}
|
|
|
|
if (transactionSubtype === "receive") {
|
|
// Money received → green
|
|
return "bg-green-50";
|
|
}
|
|
|
|
if (transactionSubtype === "send") {
|
|
// Money sent → blue
|
|
return "bg-blue-50";
|
|
}
|
|
|
|
if (transactionSubtype === "add_cash") {
|
|
// Add cash → blue
|
|
return "bg-blue-50";
|
|
}
|
|
|
|
// Event notifications
|
|
if (
|
|
rawType === "event" ||
|
|
rawType === "event_ticket_purchased" ||
|
|
rawType === "event_reminder" ||
|
|
notification.category === "event"
|
|
) {
|
|
return "bg-purple-50";
|
|
}
|
|
|
|
// Money request
|
|
if (
|
|
rawType === "request_received" ||
|
|
title.includes("request") ||
|
|
message.includes("request")
|
|
) {
|
|
return "bg-yellow-50";
|
|
}
|
|
|
|
// Cash out → red
|
|
if (
|
|
rawType === "cash_out" ||
|
|
title.includes("cash out") ||
|
|
message.includes("cash out")
|
|
) {
|
|
return "bg-red-50";
|
|
}
|
|
|
|
// Money received → green
|
|
if (
|
|
rawType === "money_received" ||
|
|
rawType === "receive" ||
|
|
title.includes("received") ||
|
|
message.includes("received")
|
|
) {
|
|
return "bg-green-50";
|
|
}
|
|
|
|
// Money sent → blue
|
|
if (
|
|
rawType === "money_sent" ||
|
|
rawType === "send" ||
|
|
title.includes("sent") ||
|
|
message.includes("sent")
|
|
) {
|
|
return "bg-blue-50";
|
|
}
|
|
|
|
// Generic transaction
|
|
if (
|
|
rawType === "transaction_completed" ||
|
|
rawType === "transaction" ||
|
|
title.includes("transaction") ||
|
|
message.includes("transaction")
|
|
) {
|
|
return "bg-blue-50";
|
|
}
|
|
|
|
// Default
|
|
return "bg-gray-50";
|
|
};
|
|
|
|
const getIconBackgroundColor = () => {
|
|
const rawType = (notification.type || "").toString().toLowerCase();
|
|
const title = (notification.title || "").toString().toLowerCase();
|
|
const message = (notification.message || "").toString().toLowerCase();
|
|
const transactionSubtype = (
|
|
notification.transactionType ||
|
|
notification.transaction_type ||
|
|
notification.subtype ||
|
|
""
|
|
)
|
|
.toString()
|
|
.toLowerCase();
|
|
|
|
// --- Explicit transaction subtypes (preferred) ---
|
|
if (transactionSubtype === "cash_out") {
|
|
// Cash out → red
|
|
return "bg-red-500";
|
|
}
|
|
|
|
if (transactionSubtype === "receive") {
|
|
// Money received → green
|
|
return "bg-green-500";
|
|
}
|
|
|
|
if (transactionSubtype === "send") {
|
|
// Money sent → blue
|
|
return "bg-blue-500";
|
|
}
|
|
|
|
if (transactionSubtype === "add_cash") {
|
|
// Add cash → blue
|
|
return "bg-blue-500";
|
|
}
|
|
|
|
// Event notifications
|
|
if (
|
|
rawType === "event" ||
|
|
rawType === "event_ticket_purchased" ||
|
|
rawType === "event_reminder" ||
|
|
notification.category === "event"
|
|
) {
|
|
return "bg-purple-500";
|
|
}
|
|
|
|
// Money request → yellow
|
|
if (
|
|
rawType === "request_received" ||
|
|
title.includes("request") ||
|
|
message.includes("request")
|
|
) {
|
|
return "bg-yellow-500";
|
|
}
|
|
|
|
// Cash out → red
|
|
if (
|
|
rawType === "cash_out" ||
|
|
title.includes("cash out") ||
|
|
message.includes("cash out")
|
|
) {
|
|
return "bg-red-500";
|
|
}
|
|
|
|
// Money received → green
|
|
if (
|
|
rawType === "money_received" ||
|
|
rawType === "receive" ||
|
|
title.includes("received") ||
|
|
message.includes("received")
|
|
) {
|
|
return "bg-green-500";
|
|
}
|
|
|
|
// Money sent → blue
|
|
if (
|
|
rawType === "money_sent" ||
|
|
rawType === "send" ||
|
|
title.includes("sent") ||
|
|
message.includes("sent")
|
|
) {
|
|
return "bg-blue-500";
|
|
}
|
|
|
|
// Generic transaction → blue
|
|
if (
|
|
rawType === "transaction_completed" ||
|
|
rawType === "transaction" ||
|
|
title.includes("transaction") ||
|
|
message.includes("transaction")
|
|
) {
|
|
return "bg-blue-500";
|
|
}
|
|
|
|
// Default
|
|
return "bg-orange-200";
|
|
};
|
|
|
|
const handleMoneyRequestPress = () => {
|
|
console.log("NOTIFICATION", notification);
|
|
//@ts-ignore
|
|
if (notification.type === "request_received" && notification.requestId) {
|
|
onMoneyRequestPrompt?.(notification);
|
|
} else {
|
|
onPress?.();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<View
|
|
className={`${getNotificationColor()} rounded-lg p-4 mb-3 ${
|
|
notification.read ? "opacity-60" : ""
|
|
}`}
|
|
>
|
|
<TouchableOpacity
|
|
onPress={handleMoneyRequestPress}
|
|
className="flex-row items-center"
|
|
>
|
|
{/* Icon Avatar */}
|
|
<View className={`${getIconBackgroundColor()} rounded-full p-3 mr-4`}>
|
|
{getNotificationIcon()}
|
|
</View>
|
|
|
|
{/* Notification Content */}
|
|
<View className="flex-1">
|
|
<Text className="text-black font-dmsans-bold text-base mb-1">
|
|
{notification.title}
|
|
</Text>
|
|
<Text className="text-gray-600 font-dmsans text-sm">
|
|
{notification.message}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Chevron for money requests */}
|
|
{notification.type === "request_received" && (
|
|
<ChevronRight color="#9CA3AF" size={20} />
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default function Notification() {
|
|
const { user } = useAuthWithProfile();
|
|
const { t } = useTranslation();
|
|
const [toastVisible, setToastVisible] = React.useState(false);
|
|
const [toastTitle, setToastTitle] = React.useState("");
|
|
const [toastDescription, setToastDescription] = React.useState<
|
|
string | undefined
|
|
>(undefined);
|
|
const [toastVariant, setToastVariant] = React.useState<
|
|
"success" | "error" | "warning" | "info"
|
|
>("info");
|
|
const toastTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(
|
|
null
|
|
);
|
|
|
|
const [requestModalVisible, setRequestModalVisible] = React.useState(false);
|
|
const [activeRequestNotification, setActiveRequestNotification] =
|
|
React.useState<any | null>(null);
|
|
|
|
const showToast = (
|
|
title: string,
|
|
description?: string,
|
|
variant: "success" | "error" | "warning" | "info" = "info"
|
|
) => {
|
|
if (toastTimeoutRef.current) {
|
|
clearTimeout(toastTimeoutRef.current);
|
|
}
|
|
|
|
setToastTitle(title);
|
|
setToastDescription(description);
|
|
setToastVariant(variant);
|
|
setToastVisible(true);
|
|
|
|
toastTimeoutRef.current = setTimeout(() => {
|
|
setToastVisible(false);
|
|
toastTimeoutRef.current = null;
|
|
}, 2500);
|
|
};
|
|
const {
|
|
notifications,
|
|
loading,
|
|
error,
|
|
unreadCount,
|
|
markAsRead,
|
|
markAllAsRead,
|
|
} = useNotifications(user?.uid || null);
|
|
|
|
const handleMarkAsRead = async () => {
|
|
await markAllAsRead();
|
|
};
|
|
|
|
const handleNotificationPress = async (notification: any) => {
|
|
if (notification?.id) {
|
|
await markAsRead(notification.id);
|
|
}
|
|
|
|
const transactionId =
|
|
notification?.transactionId || notification?.transaction_id;
|
|
|
|
const transactionSubtype =
|
|
notification?.transactionType ||
|
|
notification?.transaction_type ||
|
|
notification?.subtype;
|
|
|
|
if (
|
|
transactionId &&
|
|
(notification?.type === "transaction_completed" ||
|
|
notification?.type === "money_received" ||
|
|
notification?.type === "money_sent" ||
|
|
notification?.category === "transaction")
|
|
) {
|
|
router.push({
|
|
pathname: ROUTES.TRANSACTION_DETAIL,
|
|
params: {
|
|
transactionId,
|
|
type: transactionSubtype || "",
|
|
amount:
|
|
notification?.amount !== undefined && notification?.amount !== null
|
|
? String(notification.amount)
|
|
: "",
|
|
recipientName:
|
|
notification?.recipientName || notification?.counterpartyName || "",
|
|
date:
|
|
(notification?.createdAt &&
|
|
typeof notification.createdAt === "string" &&
|
|
notification.createdAt) ||
|
|
(notification?.date && typeof notification.date === "string"
|
|
? notification.date
|
|
: ""),
|
|
status: notification?.status || "",
|
|
note: notification?.note || "",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For other notification types (e.g. money requests), we rely on their specific handlers
|
|
};
|
|
|
|
const handleMoneyRequestAction = async (
|
|
requestId: string,
|
|
action: "accept" | "decline"
|
|
) => {
|
|
try {
|
|
// Get the request details
|
|
const requestResult = await RequestService.getRequestById(requestId);
|
|
if (!requestResult.success || !requestResult.request) {
|
|
console.error("Failed to get request:", requestResult.error);
|
|
return;
|
|
}
|
|
|
|
const request = requestResult.request;
|
|
|
|
if (request.status !== "pending") {
|
|
console.error("Request is not pending");
|
|
showToast(
|
|
t("notification.toastErrorTitle"),
|
|
t("notification.toastRequestNotPending"),
|
|
"error"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Get requestor details (the person who made the request)
|
|
const requestorResult = await RequestService.getUserByPhoneNumber(
|
|
request.requestorPhoneNumber
|
|
);
|
|
if (!requestorResult.success || !requestorResult.user) {
|
|
console.error(
|
|
"Failed to get requestor details:",
|
|
requestorResult.error
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Get requestee details (the person receiving the request - current user)
|
|
const requesteeResult = await RequestService.getUserByPhoneNumber(
|
|
request.requesteePhoneNumber
|
|
);
|
|
if (!requesteeResult.success || !requesteeResult.user) {
|
|
console.error(
|
|
"Failed to get requestee details:",
|
|
requesteeResult.error
|
|
);
|
|
return;
|
|
}
|
|
|
|
const result = await NotificationService.acceptDeclineMoneyRequest(
|
|
requestId,
|
|
requesteeResult.user.uid, // requesteeUid (current user)
|
|
requestorResult.user.uid, // requestorUid (person who made the request)
|
|
requestorResult.user.displayName, // requestorName
|
|
requesteeResult.user.displayName, // requesteeName
|
|
request.amount, // actual amount from request
|
|
action
|
|
);
|
|
|
|
if (result.success) {
|
|
console.log(`Request ${action}ed successfully`);
|
|
|
|
if (action === "accept") {
|
|
// Navigate to success screen only for accepted requests
|
|
router.replace({
|
|
pathname: ROUTES.MONEY_DONATED,
|
|
params: {
|
|
message: `Congratulations! Transaction completed on your end.`,
|
|
amount: (request.amount / 100).toFixed(2),
|
|
recipientName: requestorResult.user!.displayName,
|
|
},
|
|
});
|
|
} else {
|
|
// For declined requests, show a simple confirmation toast
|
|
showToast(
|
|
t("notification.toastInfoTitle", "Request Declined"),
|
|
t(
|
|
"notification.toastRequestDeclined",
|
|
`You have declined the money request from ${
|
|
requestorResult.user!.displayName
|
|
}.`
|
|
),
|
|
"info"
|
|
);
|
|
}
|
|
} else {
|
|
console.error(`Failed to ${action} request:`, result.error);
|
|
showToast(
|
|
t("notification.toastErrorTitle"),
|
|
t("notification.toastRequestActionFailed", { action }),
|
|
"error"
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error ${action}ing request:`, error);
|
|
showToast(
|
|
t("notification.toastErrorTitle"),
|
|
t("notification.toastRequestActionFailed", { action }),
|
|
"error"
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleMoneyRequestPrompt = (notification: any) => {
|
|
setActiveRequestNotification(notification);
|
|
setRequestModalVisible(true);
|
|
};
|
|
|
|
return (
|
|
<ScreenWrapper edges={[]}>
|
|
<BackButton />
|
|
<View className="flex-row items-center justify-center px-5 py-4">
|
|
<Text className="text-2xl font-dmsans-bold text-black">
|
|
{t("notification.title")}
|
|
</Text>
|
|
</View>
|
|
|
|
<ScrollView className="flex-1 px-5">
|
|
{/* Today Section */}
|
|
<View className="py-4">
|
|
<View className="flex-row items-center justify-between">
|
|
<View>
|
|
<Text className="text-sm text-gray-500 font-dmsans mb-4">
|
|
{t("notification.sectionToday")}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{loading ? (
|
|
<View className="flex items-center justify-center py-10">
|
|
<Text className="text-gray-500 font-dmsans">
|
|
{t("notification.loading")}
|
|
</Text>
|
|
</View>
|
|
) : error ? (
|
|
<View className="flex items-center justify-center py-10">
|
|
<Text className="text-red-500 font-dmsans">
|
|
{t("notification.errorWithMessage", { error })}
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
<FlatList
|
|
data={notifications}
|
|
keyExtractor={(item) => item.id}
|
|
scrollEnabled={false}
|
|
ItemSeparatorComponent={() => <View className="h-2" />}
|
|
renderItem={({ item }) => (
|
|
<NotificationCard
|
|
notification={item}
|
|
onPress={() => handleNotificationPress(item)}
|
|
onMoneyRequestAction={handleMoneyRequestAction}
|
|
onMoneyRequestPrompt={handleMoneyRequestPrompt}
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
</View>
|
|
|
|
{/* Empty state if no notifications */}
|
|
{!loading && !error && notifications.length === 0 && (
|
|
<View className="flex-1 items-center justify-center py-20">
|
|
<View className="bg-gray-100 rounded-full p-6 mb-4">
|
|
<User color="#9CA3AF" size={32} />
|
|
</View>
|
|
<Text className="text-gray-500 font-dmsans text-lg mb-2">
|
|
{t("notification.emptyTitle")}
|
|
</Text>
|
|
<Text className="text-gray-400 font-dmsans text-sm text-center px-8">
|
|
{t("notification.emptySubtitle")}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
<ModalToast
|
|
visible={toastVisible}
|
|
title={toastTitle}
|
|
description={toastDescription}
|
|
variant={toastVariant}
|
|
/>
|
|
|
|
<PermissionAlertModal
|
|
visible={requestModalVisible}
|
|
title={t("notification.moneyRequestTitle", "Money Request")}
|
|
message={activeRequestNotification?.message || ""}
|
|
primaryText={t("notification.moneyRequestAccept", "Accept")}
|
|
secondaryText={t("notification.moneyRequestDecline", "Decline")}
|
|
onPrimary={() => {
|
|
if (activeRequestNotification?.requestId) {
|
|
handleMoneyRequestAction(
|
|
activeRequestNotification.requestId,
|
|
"accept"
|
|
);
|
|
}
|
|
setRequestModalVisible(false);
|
|
setActiveRequestNotification(null);
|
|
}}
|
|
onSecondary={() => {
|
|
if (activeRequestNotification?.requestId) {
|
|
handleMoneyRequestAction(
|
|
activeRequestNotification.requestId,
|
|
"decline"
|
|
);
|
|
}
|
|
setRequestModalVisible(false);
|
|
setActiveRequestNotification(null);
|
|
}}
|
|
/>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|