-
- Powerful Features for Modern Finance
-
-
-
- CalxSecure handles everything from peer-to-peer transactions to
- automated bank webhooks — built to scale beyond a million users.
+
+
+
+
+ — with
+ confidence.
+
+
+
+ Experience effortless payments built for security and speed.
+ CalxSecure empowers you to handle every transaction with total
+ trust.
+
+ {status === "authenticated" && (
+ router.push("/dashboard")}>
+ Go to Dashboard
+
+ )}
+ {status === "unauthenticated" && (
+ router.push("/auth/signup")}>
+ Get Started
+
+ )}
+
-
-
-
+
{/* Security Section */}
-
- {/* Background Pattern */}
-
-
-
+
+
{/* Header */}
-
+
+
🔒 Bank-Grade Protection
Enterprise-Level Security
-
+
Your funds are protected with military-grade encryption,
AI-powered fraud detection, and 24/7 monitoring—trusted by
millions worldwide.
@@ -162,7 +138,7 @@ const page = () => {
{/* Features Grid */}
-
+
{S1.map((item, index) => (
{
viewport={{ once: true }}
className="group relative"
>
-
+
{/* Icon */}
@@ -183,28 +159,30 @@ const page = () => {
{/* Content */}
-
+
{item.title}
-
+
{item.description || "Advanced protection"}
-
-
))}
+
+
-
-
+
{/* CTA Section */}
-
+
Ready to Get
@@ -214,10 +192,10 @@ const page = () => {
Join thousands of users and merchants powering their payments with
CalxSecure
-
+
{status === "authenticated" && (
- router.push("/qr")}>
- Generate QR Code
+ router.push("/dashboard")}>
+ Go to Dashboard
)}
{status === "unauthenticated" && (
@@ -230,11 +208,11 @@ const page = () => {
{/* Footer */}
-
+
© 2025 CalxSecure. All rights reserved.
- )
-}
+ );
+};
-export default page
+export default Page;
diff --git a/apps/merchant-app/components/molecules/QRPaymentHero.tsx b/apps/merchant-app/components/molecules/QRPaymentHero.tsx
index 17e675e..11e3812 100644
--- a/apps/merchant-app/components/molecules/QRPaymentHero.tsx
+++ b/apps/merchant-app/components/molecules/QRPaymentHero.tsx
@@ -7,6 +7,7 @@ import { Button } from "../atoms/Button";
import { motion } from "framer-motion";
import { LampContainer } from "../ui/lamp";
import { TextInput } from "@repo/ui/textinput";
+import { HoverBorderGradient } from "../ui/hover-border-gradient";
export default function QRPaymentHero() {
const [amount, setAmount] = useState("");
@@ -61,15 +62,15 @@ export default function QRPaymentHero() {
return (
-
+
-
+
{/* FORM — LEFT SIDE */}
setAmount(value)}
- className="text-zinc-100 border-zinc-600"
+ className=" border-zinc-400 dark:border-zinc-600"
/>
setDescription(value.slice(0, 20))}
- className="text-zinc-100 border-zinc-600"
+ className="border-zinc-400 dark:border-zinc-600"
/>
-
+
{generateQR.isPending ? "Generating..." : "Generate QR Code"}
-
+
+
+
+
{/* QR — APPEARS ON RIGHT SIDE */}
{qrData && (
@@ -107,7 +111,7 @@ export default function QRPaymentHero() {
className="w-72 h-72"
style={{ backgroundColor: "#27272A" }}
/>
-
+
₹{amount}
7822952595@ibl
@@ -125,7 +129,7 @@ export default function QRPaymentHero() {
.catch((err) => alert(`Confirmation failed: ${err.response?.data?.error || err.message}`));
}
}}
- className="mt-4 w-full bg-green-600 hover:bg-green-700"
+ className="mt-4 w-full bg-zinc-600 hover:bg-zinc-700"
>
Confirm Payment
diff --git a/apps/merchant-app/components/ui/background-beams-with-collision.tsx b/apps/merchant-app/components/ui/background-beams-with-collision.tsx
new file mode 100644
index 0000000..2339224
--- /dev/null
+++ b/apps/merchant-app/components/ui/background-beams-with-collision.tsx
@@ -0,0 +1,260 @@
+"use client";
+
+import { motion, AnimatePresence } from "motion/react";
+import React, { useRef, useState, useEffect } from "react";
+import { cn } from "../../lib/utils";
+
+export const BackgroundBeamsWithCollision = ({
+ children,
+ className,
+}: {
+ children: React.ReactNode;
+ className?: string;
+}) => {
+ const containerRef = useRef(null);
+ const parentRef = useRef(null);
+
+ const beams = [
+ {
+ initialX: 10,
+ translateX: 10,
+ duration: 7,
+ repeatDelay: 3,
+ delay: 2,
+ },
+ {
+ initialX: 600,
+ translateX: 600,
+ duration: 3,
+ repeatDelay: 3,
+ delay: 4,
+ },
+ {
+ initialX: 100,
+ translateX: 100,
+ duration: 7,
+ repeatDelay: 7,
+ className: "h-6",
+ },
+ {
+ initialX: 400,
+ translateX: 400,
+ duration: 5,
+ repeatDelay: 14,
+ delay: 4,
+ },
+ {
+ initialX: 800,
+ translateX: 800,
+ duration: 11,
+ repeatDelay: 2,
+ className: "h-20",
+ },
+ {
+ initialX: 1000,
+ translateX: 1000,
+ duration: 4,
+ repeatDelay: 2,
+ className: "h-12",
+ },
+ {
+ initialX: 1200,
+ translateX: 1200,
+ duration: 6,
+ repeatDelay: 4,
+ delay: 2,
+ className: "h-6",
+ },
+ ];
+
+ return (
+
+ {beams.map((beam) => (
+
+ ))}
+
+ {children}
+
+
+ );
+};
+
+const CollisionMechanism = React.forwardRef<
+ HTMLDivElement,
+ {
+ containerRef: React.RefObject;
+ parentRef: React.RefObject;
+ beamOptions?: {
+ initialX?: number;
+ translateX?: number;
+ initialY?: number;
+ translateY?: number;
+ rotate?: number;
+ className?: string;
+ duration?: number;
+ delay?: number;
+ repeatDelay?: number;
+ };
+ }
+>(({ parentRef, containerRef, beamOptions = {} }, ref) => {
+ const beamRef = useRef(null);
+ const [collision, setCollision] = useState<{
+ detected: boolean;
+ coordinates: { x: number; y: number } | null;
+ }>({
+ detected: false,
+ coordinates: null,
+ });
+ const [beamKey, setBeamKey] = useState(0);
+ const [cycleCollisionDetected, setCycleCollisionDetected] = useState(false);
+
+ useEffect(() => {
+ const checkCollision = () => {
+ if (
+ beamRef.current &&
+ containerRef.current &&
+ parentRef.current &&
+ !cycleCollisionDetected
+ ) {
+ const beamRect = beamRef.current.getBoundingClientRect();
+ const containerRect = containerRef.current.getBoundingClientRect();
+ const parentRect = parentRef.current.getBoundingClientRect();
+
+ if (beamRect.bottom >= containerRect.top) {
+ const relativeX =
+ beamRect.left - parentRect.left + beamRect.width / 2;
+ const relativeY = beamRect.bottom - parentRect.top;
+
+ setCollision({
+ detected: true,
+ coordinates: {
+ x: relativeX,
+ y: relativeY,
+ },
+ });
+ setCycleCollisionDetected(true);
+ }
+ }
+ };
+
+ const animationInterval = setInterval(checkCollision, 50);
+
+ return () => clearInterval(animationInterval);
+ }, [cycleCollisionDetected, containerRef]);
+
+ useEffect(() => {
+ if (collision.detected && collision.coordinates) {
+ setTimeout(() => {
+ setCollision({ detected: false, coordinates: null });
+ setCycleCollisionDetected(false);
+ }, 2000);
+
+ setTimeout(() => {
+ setBeamKey((prevKey) => prevKey + 1);
+ }, 2000);
+ }
+ }, [collision]);
+
+ return (
+ <>
+
+
+ {collision.detected && collision.coordinates && (
+
+ )}
+
+ >
+ );
+});
+
+CollisionMechanism.displayName = "CollisionMechanism";
+
+const Explosion = ({ ...props }: React.HTMLProps) => {
+ const spans = Array.from({ length: 20 }, (_, index) => ({
+ id: index,
+ initialX: 0,
+ initialY: 0,
+ directionX: Math.floor(Math.random() * 80 - 40),
+ directionY: Math.floor(Math.random() * -50 - 10),
+ }));
+
+ return (
+
+
+ {spans.map((span) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/merchant-app/components/ui/floating-navbar.tsx b/apps/merchant-app/components/ui/floating-navbar.tsx
index 2a38e1b..e0b38e2 100644
--- a/apps/merchant-app/components/ui/floating-navbar.tsx
+++ b/apps/merchant-app/components/ui/floating-navbar.tsx
@@ -1,7 +1,6 @@
"use client";
import React from "react";
import { motion } from "motion/react";
-import { cn } from "../../lib/utils";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
@@ -9,6 +8,11 @@ import { Avatar, AvatarFallback, AvatarImage } from "../../../../packages/ui/src
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "./dropdown-menu";
import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu";
import { TextHoverEffect } from "../../../../packages/ui/src/text-hover-effect";
+import { useTheme } from "next-themes";
+import { Moon, Sun } from "lucide-react";
+import Link from "next/link";
+import { cn } from "../../lib/utils";
+import { Button } from "@repo/ui/button";
export const FloatingNav = ({
navItems,
@@ -21,6 +25,7 @@ export const FloatingNav = ({
}[];
className?: string;
}) => {
+ const { theme , setTheme } = useTheme();
const { data: session, status } = useSession();
const router = useRouter();
@@ -49,7 +54,7 @@ export const FloatingNav = ({
>
{navItems.map((navItem, idx) => (
-
{navItem.icon && {navItem.icon} }
{navItem.name}
-
+
))}
{ status === "unauthenticated" && (
@@ -72,7 +77,7 @@ export const FloatingNav = ({
-
+
{session?.user?.name?.[0] || "US"}
@@ -84,9 +89,20 @@ export const FloatingNav = ({
+
)}
-
+
+ {theme === "light" ? (
+ setTheme("dark")} className=" bg-zinc-800 hover:bg-zinc-700 rounded-full py-2" >
+
+
+ ) : (
+ setTheme("light")}className=" bg-zinc-100 hover:bg-zinc-200 rounded-full py-2" >
+
+
+ )}
+
);
diff --git a/apps/merchant-app/components/ui/shooting-stars.tsx b/apps/merchant-app/components/ui/shooting-stars.tsx
new file mode 100644
index 0000000..9bac384
--- /dev/null
+++ b/apps/merchant-app/components/ui/shooting-stars.tsx
@@ -0,0 +1,147 @@
+"use client";
+
+import React, { useEffect, useState, useRef } from "react";
+import { cn } from "../../lib/utils";
+
+interface ShootingStar {
+ id: number;
+ x: number;
+ y: number;
+ angle: number;
+ scale: number;
+ speed: number;
+ distance: number;
+}
+
+interface ShootingStarsProps {
+ minSpeed?: number;
+ maxSpeed?: number;
+ minDelay?: number;
+ maxDelay?: number;
+ starColor?: string;
+ trailColor?: string;
+ starWidth?: number;
+ starHeight?: number;
+ className?: string;
+}
+
+const getRandomStartPoint = () => {
+ const side = Math.floor(Math.random() * 4);
+ const offset = Math.random() * window.innerWidth;
+
+ switch (side) {
+ case 0:
+ return { x: offset, y: 0, angle: 45 };
+ case 1:
+ return { x: window.innerWidth, y: offset, angle: 135 };
+ case 2:
+ return { x: offset, y: window.innerHeight, angle: 225 };
+ case 3:
+ return { x: 0, y: offset, angle: 315 };
+ default:
+ return { x: 0, y: 0, angle: 45 };
+ }
+};
+export const ShootingStars: React.FC = ({
+ minSpeed = 10,
+ maxSpeed = 30,
+ minDelay = 1200,
+ maxDelay = 4200,
+ starColor = "#9E00FF",
+ trailColor = "#2EB9DF",
+ starWidth = 10,
+ starHeight = 1,
+ className,
+}) => {
+ const [star, setStar] = useState(null);
+ const svgRef = useRef(null);
+
+ useEffect(() => {
+ const createStar = () => {
+ const { x, y, angle } = getRandomStartPoint();
+ const newStar: ShootingStar = {
+ id: Date.now(),
+ x,
+ y,
+ angle,
+ scale: 1,
+ speed: Math.random() * (maxSpeed - minSpeed) + minSpeed,
+ distance: 0,
+ };
+ setStar(newStar);
+
+ const randomDelay = Math.random() * (maxDelay - minDelay) + minDelay;
+ setTimeout(createStar, randomDelay);
+ };
+
+ createStar();
+
+ return () => {};
+ }, [minSpeed, maxSpeed, minDelay, maxDelay]);
+
+ useEffect(() => {
+ const moveStar = () => {
+ if (star) {
+ setStar((prevStar) => {
+ if (!prevStar) return null;
+ const newX =
+ prevStar.x +
+ prevStar.speed * Math.cos((prevStar.angle * Math.PI) / 180);
+ const newY =
+ prevStar.y +
+ prevStar.speed * Math.sin((prevStar.angle * Math.PI) / 180);
+ const newDistance = prevStar.distance + prevStar.speed;
+ const newScale = 1 + newDistance / 100;
+ if (
+ newX < -20 ||
+ newX > window.innerWidth + 20 ||
+ newY < -20 ||
+ newY > window.innerHeight + 20
+ ) {
+ return null;
+ }
+ return {
+ ...prevStar,
+ x: newX,
+ y: newY,
+ distance: newDistance,
+ scale: newScale,
+ };
+ });
+ }
+ };
+
+ const animationFrame = requestAnimationFrame(moveStar);
+ return () => cancelAnimationFrame(animationFrame);
+ }, [star]);
+
+ return (
+
+ {star && (
+
+ )}
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/merchant-app/components/ui/stars-background.tsx b/apps/merchant-app/components/ui/stars-background.tsx
new file mode 100644
index 0000000..368076d
--- /dev/null
+++ b/apps/merchant-app/components/ui/stars-background.tsx
@@ -0,0 +1,144 @@
+"use client";
+
+import React, {
+ useState,
+ useEffect,
+ useRef,
+ RefObject,
+ useCallback,
+} from "react";
+import { cn } from "../../lib/utils";
+
+interface StarProps {
+ x: number;
+ y: number;
+ radius: number;
+ opacity: number;
+ twinkleSpeed: number | null;
+}
+
+interface StarBackgroundProps {
+ starDensity?: number;
+ allStarsTwinkle?: boolean;
+ twinkleProbability?: number;
+ minTwinkleSpeed?: number;
+ maxTwinkleSpeed?: number;
+ className?: string;
+}
+
+export const StarsBackground: React.FC = ({
+ starDensity = 0.00015,
+ allStarsTwinkle = true,
+ twinkleProbability = 0.7,
+ minTwinkleSpeed = 0.5,
+ maxTwinkleSpeed = 1,
+ className,
+}) => {
+ const [stars, setStars] = useState([]);
+ const canvasRef: RefObject =
+ useRef(null);
+
+ const generateStars = useCallback(
+ (width: number, height: number): StarProps[] => {
+ const area = width * height;
+ const numStars = Math.floor(area * starDensity);
+ return Array.from({ length: numStars }, () => {
+ const shouldTwinkle =
+ allStarsTwinkle || Math.random() < twinkleProbability;
+ return {
+ x: Math.random() * width,
+ y: Math.random() * height,
+ radius: Math.random() * 0.05 + 0.5,
+ opacity: Math.random() * 0.5 + 0.5,
+ twinkleSpeed: shouldTwinkle
+ ? minTwinkleSpeed +
+ Math.random() * (maxTwinkleSpeed - minTwinkleSpeed)
+ : null,
+ };
+ });
+ },
+ [
+ starDensity,
+ allStarsTwinkle,
+ twinkleProbability,
+ minTwinkleSpeed,
+ maxTwinkleSpeed,
+ ]
+ );
+
+ useEffect(() => {
+ const updateStars = () => {
+ if (canvasRef.current) {
+ const canvas = canvasRef.current;
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+
+ const { width, height } = canvas.getBoundingClientRect();
+ canvas.width = width;
+ canvas.height = height;
+ setStars(generateStars(width, height));
+ }
+ };
+
+ updateStars();
+
+ const resizeObserver = new ResizeObserver(updateStars);
+ if (canvasRef.current) {
+ resizeObserver.observe(canvasRef.current);
+ }
+
+ return () => {
+ if (canvasRef.current) {
+ resizeObserver.unobserve(canvasRef.current);
+ }
+ };
+ }, [
+ starDensity,
+ allStarsTwinkle,
+ twinkleProbability,
+ minTwinkleSpeed,
+ maxTwinkleSpeed,
+ generateStars,
+ ]);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+
+ let animationFrameId: number;
+
+ const render = () => {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ stars.forEach((star) => {
+ ctx.beginPath();
+ ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
+ ctx.fillStyle = `rgba(255, 255, 255, ${star.opacity})`;
+ ctx.fill();
+
+ if (star.twinkleSpeed !== null) {
+ star.opacity =
+ 0.5 +
+ Math.abs(Math.sin((Date.now() * 0.001) / star.twinkleSpeed) * 0.5);
+ }
+ });
+
+ animationFrameId = requestAnimationFrame(render);
+ };
+
+ render();
+
+ return () => {
+ cancelAnimationFrame(animationFrameId);
+ };
+ }, [stars]);
+
+ return (
+
+ );
+};
diff --git a/apps/user-app/app/(dashboard)/bills/page.tsx b/apps/user-app/app/(dashboard)/bills/page.tsx
index f706e73..4d8f2e3 100644
--- a/apps/user-app/app/(dashboard)/bills/page.tsx
+++ b/apps/user-app/app/(dashboard)/bills/page.tsx
@@ -1,125 +1,148 @@
"use client";
- import { useEffect, useState } from "react";
- import { BillPaymentCard } from "@/components/BillPaymentCard";
- import { Button } from "@/components/ui/button";
+import { useEffect, useState } from "react";
+import { BillPaymentCard } from "@/components/BillPaymentCard";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
- type BillSchedule = {
- id: number;
- userId: number;
- merchantId?: number;
- billType: string;
- provider: string;
- accountNo: string;
- amount: number;
- dueDate: string;
- nextPayment?: string;
- paymentMethod: string;
- status: "PENDING" | "PAID" | "OVERDUE";
- token?: string;
- };
+type BillSchedule = {
+ id: number;
+ userId: number;
+ merchantId?: number;
+ billType: string;
+ provider: string;
+ accountNo: string;
+ amount: number;
+ dueDate: string;
+ nextPayment?: string;
+ paymentMethod: string;
+ status: "PENDING" | "PAID" | "OVERDUE";
+ token?: string;
+};
- export default function BillsPage() {
- const [schedules, setSchedules] = useState([]);
- const [error, setError] = useState("");
- const userId = 1; // Replace with authenticated user ID
+export default function BillsPage() {
+ const [schedules, setSchedules] = useState([]);
+ const [error, setError] = useState("");
+ const userId = 1; // Replace with authenticated user ID
- const fetchBills = async () => {
- try {
- const response = await fetch(`/api/bills?userId=${userId}`);
- if (!response.ok) throw new Error(`Failed to fetch bills: ${response.statusText}`);
- const data = await response.json();
- setSchedules(data);
- setError("");
- } catch (error: any) {
- console.error("Error fetching bills:", error);
- setError("Failed to load bills. Please try again.");
- }
- };
+ const fetchBills = async () => {
+ try {
+ const response = await fetch(`/api/bills?userId=${userId}`);
+ if (!response.ok)
+ throw new Error(`Failed to fetch bills: ${response.statusText}`);
+ const data = await response.json();
+ setSchedules(data);
+ setError("");
+ } catch (error: any) {
+ console.error("Error fetching bills:", error);
+ setError("Failed to load bills. Please try again.");
+ }
+ };
- useEffect(() => {
- fetchBills();
- }, []);
+ useEffect(() => {
+ fetchBills();
+ }, []);
- return (
-
-
-
Bill Payments
-
-
-
-
-
+ return (
+
+
Manage Bills
-
-
-
- Upcoming Bills ({schedules.length})
-
-
- Refresh Bills
-
-
- {error && (
-
- {error}
-
- )}
-
- {schedules
- .sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime())
- .map((schedule) => {
- const dueDate = new Date(schedule.dueDate);
- const isOverdue = dueDate < new Date() && schedule.status !== "PAID";
+
+
- return (
-
-
{schedule.billType}
-
{schedule.provider}
-
- ₹{(schedule.amount / 100).toFixed(2)}
-
-
- Due: {dueDate.toLocaleDateString("en-IN", {
- day: "numeric",
- month: "long",
- year: "numeric",
- })}
-
-
- Status: {schedule.status}
-
-
- Payment Method: {schedule.paymentMethod}
-
- {schedule.nextPayment && (
-
- Next Payment: {new Date(schedule.nextPayment).toLocaleDateString("en-IN", {
+
+
+
+ Upcoming Bills ({schedules.length})
+
+
+ Refresh Bills
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {schedules
+ .sort(
+ (a, b) =>
+ new Date(a.dueDate).getTime() -
+ new Date(b.dueDate).getTime()
+ )
+ .map((schedule) => {
+ const dueDate = new Date(schedule.dueDate);
+ const isOverdue =
+ dueDate < new Date() && schedule.status !== "PAID";
+
+ return (
+
+
+ {schedule.billType}
+
+
+ {schedule.provider}
+
+
+ ₹{(schedule.amount / 100).toFixed(2)}
+
+
+ Due:{" "}
+ {dueDate.toLocaleDateString("en-IN", {
+ day: "numeric",
+ month: "long",
+ year: "numeric",
+ })}
+
+
+ Status:{" "}
+
+ {schedule.status}
+
+
+
+ Payment Method: {schedule.paymentMethod}
+
+ {schedule.nextPayment && (
+
+ Next Payment:{" "}
+ {new Date(schedule.nextPayment).toLocaleDateString(
+ "en-IN",
+ {
day: "numeric",
month: "long",
year: "numeric",
- })}
-
- )}
- {isOverdue && (
-
- 🚨 Overdue
-
- )}
-
- );
- })}
-
-
-
-
-
-
-
- );
- }
\ No newline at end of file
+ }
+ )}
+
+ )}
+ {isOverdue && (
+
+ Overdue
+
+ )}
+
+ );
+ })}
+
+
+
+
+
+ );
+}
diff --git a/apps/user-app/app/(dashboard)/dashboard/page.tsx b/apps/user-app/app/(dashboard)/dashboard/page.tsx
index d8dcbed..6fac575 100644
--- a/apps/user-app/app/(dashboard)/dashboard/page.tsx
+++ b/apps/user-app/app/(dashboard)/dashboard/page.tsx
@@ -1,115 +1,155 @@
import { getServerSession } from "next-auth";
import prisma from "@repo/db/client";
import { BalanceCard } from "@/components/BalanceCard";
-import { TransferList } from "@/components/TransferList";
-import { OnRampList } from "@/components/OnRampList";
import { StatsCards } from "@/components/StatsCards";
import { DashboardClient } from "@/components/DashboardClient";
-import { Avatar, AvatarFallback, AvatarImage } from "../../../../../packages/ui/src/avatar";
import { Toaster } from "react-hot-toast";
import { authOptions } from "@/app/lib/auth";
import { startOfDay, subDays, format } from "date-fns";
+import ReturnPendingList from "@/components/ReturnPendingList";
+import { Avatar, AvatarFallback, AvatarImage } from "../../../../../packages/ui/src/avatar";
async function getDashboardData(userId: number) {
const balance = await prisma.balance.findFirst({ where: { userId } });
+ // 1. P2P Transfers (exclude refunded sent)
const transfers = await prisma.p2pTransfer.findMany({
where: {
OR: [{ fromUserId: userId }, { toUserId: userId }],
+ NOT: { status: "REFUNDED", fromUserId: userId },
},
- include: { fromUser: true, toUser: true },
+ include: { fromUser: true, toUser: true, wrongSendRequest: true },
orderBy: { timestamp: "desc" },
- take: 5,
+ take: 10,
+ });
+
+ // 2. Pending Returns (receiver side)
+ const pendingReturns = await prisma.wrongSendRequest.findMany({
+ where: {
+ transaction: { toUserId: userId },
+ status: "PENDING",
+ expiresAt: { gt: new Date() },
+ },
+ include: {
+ sender: { select: { name: true } },
+ transaction: { select: { amount: true, timestamp: true } },
+ },
+ orderBy: { expiresAt: "asc" },
});
+ // 3. OnRamps
const onRamps = await prisma.onRampTransaction.findMany({
where: { userId },
orderBy: { startTime: "desc" },
take: 5,
});
- // Aggregate data for chart (last 6 days)
- const startDate = startOfDay(subDays(new Date(), 5));
- const endDate = startOfDay(new Date());
-
- const sentTransfers = await prisma.p2pTransfer.groupBy({
+ // 4. Chart data
+ const start = startOfDay(subDays(new Date(), 5));
+ const sent = await prisma.p2pTransfer.groupBy({
by: ["timestamp"],
- where: { fromUserId: userId, timestamp: { gte: startDate, lte: endDate } },
+ where: { fromUserId: userId, timestamp: { gte: start } },
_sum: { amount: true },
orderBy: { timestamp: "asc" },
});
-
- const receivedTransfers = await prisma.p2pTransfer.groupBy({
+ const received = await prisma.p2pTransfer.groupBy({
by: ["timestamp"],
- where: { toUserId: userId, timestamp: { gte: startDate, lte: endDate } },
+ where: { toUserId: userId, timestamp: { gte: start } },
_sum: { amount: true },
orderBy: { timestamp: "asc" },
});
- const onRampTransactions = await prisma.onRampTransaction.groupBy({
- by: ["startTime"],
- where: { userId, startTime: { gte: startDate, lte: endDate } },
- _sum: { amount: true },
- orderBy: { startTime: "asc" },
- });
-
- // Generate labels and data for chart
- const labels = Array.from({ length: 6 }, (_, i) => format(subDays(new Date(), 5 - i), "yyyy-MM-dd"));
- const sentData = labels.map((date) => {
- const dayData = sentTransfers.find((t) => format(t.timestamp, "yyyy-MM-dd") === date);
- return (dayData?._sum?.amount || 0) / 100;
- });
- const receivedData = labels.map((date) => {
- const dayData = receivedTransfers.find((t) => format(t.timestamp, "yyyy-MM-dd") === date);
- return (dayData?._sum?.amount || 0) / 100;
- });
- const onRampData = labels.map((date) => {
- const dayData = onRampTransactions.find((t) => format(t.startTime, "yyyy-MM-dd") === date);
- return (dayData?._sum?.amount || 0) / 100;
- });
+ const labels = Array.from({ length: 6 }, (_, i) => format(subDays(new Date(), 5 - i), "MMM dd"));
+ const sentData = labels.map(d => (sent.find(t => format(t.timestamp, "MMM dd") === d)?._sum?.amount || 0) / 100);
+ const receivedData = labels.map(d => (received.find(t => format(t.timestamp, "MMM dd") === d)?._sum?.amount || 0) / 100);
return {
balance: { amount: balance?.amount || 0, locked: balance?.locked || 0 },
transfers,
+ pendingReturns: pendingReturns.map(r => ({
+ id: r.id,
+ senderName: r.sender.name ?? "Unknown",
+ amount: Number(r.transaction.amount) / 100,
+ expiresAt: r.expiresAt,
+ timestamp: r.transaction.timestamp,
+ })),
onRamps,
- chartData: { labels, sentData, receivedData, onRampData },
+ chartData: { labels, sentData, receivedData },
};
}
export default async function DashboardPage() {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
- return Please log in to view your dashboard.
;
+ return Please log in.
;
}
const userId = Number(session.user.id);
- const { balance, transfers, onRamps } = await getDashboardData(userId);
+ const { balance, transfers, pendingReturns, onRamps } = await getDashboardData(userId);
return (
-
- {/* User Greeting */}
-
+
+ {/* Greeting */}
+
-
- {session.user.name?.[0] || "U"}
+
+ {session.user.name?.[0] ?? "U"}
-
- Welcome, {session.user.name || "User"}
+
+ Hi, {session.user.name ?? "User"}
+ {/* Pending Returns */}
+ {pendingReturns.length > 0 && (
+
+
+ Pending Returns {`(${pendingReturns.length})`}
+
+
+
+
+
+ )}
- {/* Top Section */}
+ {/* Balance + Stats */}
- {/* Transfers & OnRamps */}
-
-
-
-
+
+
+ {/* Recent Activity */}
+
+ Recent Activity
+
+ {transfers.map((t) => {
+ const isSent = t.fromUserId === userId;
+ return (
+
+
+
+
+
+ {isSent ? `Sent to ${t.toUser?.name ?? "Unknown"}` : `From ${t.fromUser?.name ?? "Unknown"}`}
+
+
+ {format(new Date(t.timestamp), "MMM dd, h:mm a")}
+
+
+
+
+ {isSent ? "-" : "+"}₹{(Number(t.amount) / 100).toFixed(0)}
+
+
+ );
+ })}
+
+
diff --git a/apps/user-app/app/(dashboard)/layout.tsx b/apps/user-app/app/(dashboard)/layout.tsx
index db8ba28..0d4dcbc 100644
--- a/apps/user-app/app/(dashboard)/layout.tsx
+++ b/apps/user-app/app/(dashboard)/layout.tsx
@@ -1,73 +1,61 @@
"use client";
import { useSession } from "next-auth/react";
-import { BrickWallFire, Home, LayoutDashboard, Repeat, Send,} from 'lucide-react';
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
-} from '../../../../packages/ui/src/avatar';
-import { TooltipProvider, } from '../../../../packages/ui/src/tooltip';
+import { Home, LayoutDashboard, Send, Repeat, BrickWallFire, Wallet } from 'lucide-react';
+import { Avatar, AvatarFallback, AvatarImage } from '../../../../packages/ui/src/avatar';
+import { TooltipProvider } from '../../../../packages/ui/src/tooltip';
import { Sidebar, SidebarBody, SidebarLink, SignupBtn } from '../../../../packages/ui/src/sidebar';
+import { useMemo } from 'react';
-
-// Your Links Interface
interface Links {
label: string;
href: string;
- icon: React.JSX.Element | React.ReactNode;
+ icon: React.JSX.Element;
}
-const sidebarLinks: Links[] = [
- { label: "Home", href: "/", icon:
},
- { label: "Dashboard", href: "/dashboard", icon:
},
- { label: "Transfer", href: "/transfer", icon:
},
- { label: "P2P Transfer", href: "/p2p", icon:
},
- { label: "Bills", href: "/bills", icon:
},
-];
-
-
-export default function Layout({
- children,
-}: {
- children: React.ReactNode;
-}): JSX.Element {
+export default function Layout({ children }: { children: React.ReactNode }) {
const { data: session } = useSession();
+ const sidebarLinks = useMemo
(() => [
+ { label: "Home", href: "/", icon: },
+ { label: "Dashboard", href: "/dashboard", icon: },
+ { label: "Transfer", href: "/transfer", icon: },
+ { label: "P2P Transfer", href: "/p2p", icon: },
+ { label: "Bills", href: "/bills", icon: },
+ { label: "Recharge", href: "/recharge", icon: },
+ ], []);
+
return (
-
-
+
+
{sidebarLinks.map((link) => (
))}
-
-
-
+
+
+
{session?.user?.name?.[0] || "US"}
-
-
-
- {session?.user?.name || "Nasir Nadaf"}
+
+
+ {session?.user?.name || "Your Name"}
-
+
{session?.user?.number || "+1234567890"}
-
- {/* LOGOUT/SIGNIN */}
-
+
{children}
diff --git a/apps/user-app/app/(dashboard)/loading.tsx b/apps/user-app/app/(dashboard)/loading.tsx
new file mode 100644
index 0000000..9f1d1de
--- /dev/null
+++ b/apps/user-app/app/(dashboard)/loading.tsx
@@ -0,0 +1,14 @@
+
+import React from 'react'
+
+const loading = () => {
+ return (
+
+
+ Loading CalxSecure...
+
+
+ )
+}
+
+export default loading
diff --git a/apps/user-app/app/(dashboard)/p2p/page.tsx b/apps/user-app/app/(dashboard)/p2p/page.tsx
index 0ffcee2..c6418e0 100644
--- a/apps/user-app/app/(dashboard)/p2p/page.tsx
+++ b/apps/user-app/app/(dashboard)/p2p/page.tsx
@@ -1,30 +1,39 @@
+import { authOptions } from "@/app/lib/auth";
import { P2PTransactionHistory } from "@/components/P2pTransationsHistory";
import { SendCard } from "@/components/SendCard";
-import { CardTitle } from "@/components/ui/card";
+import { Card,} from "@/components/ui/card";
+import { HoverBorderGradient } from "@/components/ui/hover-border-gradient";
+import { getServerSession } from "next-auth";
+import Link from "next/link";
import React from "react";
-const page = () => {
+
+
+const page = async () => {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) {
+ return (
+
+
Please log in to view your transfers.
+
+
+ Login
+
+
+
+ );
+ }
return (
-
-
-
P2P Transfer
+
+
P2P Transfer
-
-
-
-
+
+
- {/* Right side - OnRamp Transaction History */}
-
-
- On-Ramp Transactions
-
-
-
+
+
+
-
);
};
diff --git a/apps/user-app/app/(dashboard)/recharge/page.tsx b/apps/user-app/app/(dashboard)/recharge/page.tsx
new file mode 100644
index 0000000..bd372cb
--- /dev/null
+++ b/apps/user-app/app/(dashboard)/recharge/page.tsx
@@ -0,0 +1,64 @@
+
+import { getServerSession } from "next-auth";
+import prisma from "@repo/db/client";
+import { authOptions } from "@/app/lib/auth";
+import RechargeForm from "@/components/RechargeForm";
+import { HoverBorderGradient } from "@/components/ui/hover-border-gradient";
+import Link from "next/link";
+
+export default async function RechargePage() {
+ const session = await getServerSession(authOptions);
+
+ if (!session?.user?.id) {
+ return (
+
+
Please log in to view your transfers.
+
+
+ Login
+
+
+
+ );
+ }
+
+ const user = await prisma.user.findUnique({
+ where: { id: Number(session.user.id) },
+ select: {
+ id: true,
+ name: true,
+ userpin: true,
+ Balance: {
+ select: {
+ amount: true,
+ locked: true,
+ },
+ },
+ },
+ });
+
+ if (!user || !user.Balance || user.Balance.length === 0) {
+ return
User not found or balance missing.
;
+ }
+
+ const balanceInRupees = Number(user.Balance[0]?.amount ?? 0) / 100;
+
+ return (
+
+
Mobile Recharge
+
+ {!user.userpin ? (
+
Please set your transaction PIN before proceeding.
+ ) : (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/apps/user-app/app/(dashboard)/return/[id]/page.tsx b/apps/user-app/app/(dashboard)/return/[id]/page.tsx
new file mode 100644
index 0000000..2992332
--- /dev/null
+++ b/apps/user-app/app/(dashboard)/return/[id]/page.tsx
@@ -0,0 +1,212 @@
+'use client';
+import { useEffect, useState, useCallback, useRef } from 'react';
+import axios from 'axios';
+import toast from 'react-hot-toast';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Timer, AlertTriangle, Lock, Eye, EyeOff, CheckCircle } from 'lucide-react';
+import confetti from 'canvas-confetti';
+import { motion } from 'framer-motion';
+
+export default function ReturnPage({ params }: { params: { id: string } }) {
+ const [request, setRequest] = useState
(null);
+ const [timeLeft, setTimeLeft] = useState(0);
+ const [showPin, setShowPin] = useState(false);
+ const [pin, setPin] = useState('');
+ const [pinInvalid, setPinInvalid] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [stage, setStage] = useState<'info' | 'pin' | 'success'>('info');
+ const pinInputRef = useRef(null);
+
+ // Fetch request
+ useEffect(() => {
+ axios.get(`/api/wrong-send/${params.id}`)
+ .then(res => {
+ setRequest(res.data);
+ const diff = new Date(res.data.expiresAt).getTime() - Date.now();
+ setTimeLeft(Math.max(0, diff));
+ })
+ .catch(() => toast.error('Invalid or expired link'));
+ }, [params.id]);
+
+ // Timer
+ useEffect(() => {
+ if (timeLeft <= 0) return;
+ const timer = setInterval(() => setTimeLeft(t => Math.max(0, t - 1000)), 1000);
+ return () => clearInterval(timer);
+ }, [timeLeft]);
+
+ // Focus PIN on open
+ useEffect(() => {
+ if (stage === 'pin' && pinInputRef.current) {
+ pinInputRef.current.focus();
+ }
+ }, [stage]);
+
+ const formatTime = (ms: number) => {
+ const h = String(Math.floor(ms / 3600000)).padStart(2, '0');
+ const m = String(Math.floor((ms % 3600000) / 60000)).padStart(2, '0');
+ const s = String(Math.floor((ms % 60000) / 1000)).padStart(2, '0');
+ return `${h}:${m}:${s}`;
+ };
+
+ const handleReturn = useCallback(() => {
+ setStage('pin');
+ }, []);
+
+ const handlePinConfirm = useCallback(async () => {
+ if (pin.length !== 4) {
+ setPinInvalid(true);
+ return;
+ }
+
+ setLoading(true);
+ try {
+ await axios.post('/api/wrong-send/approve', { requestId: params.id, pin });
+ confetti({ particleCount: 120, spread: 70, origin: { y: 0.6 } });
+ setStage('success');
+ } catch (err: any) {
+ toast.error(err.response?.data?.error || 'Wrong PIN');
+ setPin('');
+ setPinInvalid(true);
+ } finally {
+ setLoading(false);
+ }
+ }, [pin, params.id]);
+
+ if (!request) return (
+
+ );
+
+ if (stage === 'success') return (
+
+
+
+
+ ₹{request.amount} Returned!
+ Money sent back to {request.senderName}
+
+
+
+ );
+
+ return (
+
+
+
+ {stage === 'info' ? (
+ <>
+
+
+
+
+
+ {request.senderName} sent you
+
+ ₹{request.amount}
+
+ by mistake
+
+
+
+
+
+
+
+ {formatTime(timeLeft)}
+
+
+ Return now or ₹50 fee after 24h
+
+
+
+
+ {timeLeft === 0 ? '₹50 Penalty Applied' : 'Return Money Now'}
+
+
+ >
+ ) : (
+ <>
+
+
+
+ Confirm with PIN
+
+
+ Enter your 4-digit PIN to return ₹{request.amount}
+
+
+
+
+
+ {
+ setPin(e.target.value.slice(0, 4));
+ setPinInvalid(false);
+ }}
+ placeholder="••••"
+ maxLength={4}
+ className={`w-full text-center text-2xl tracking-widest font-mono bg-zinc-700 border ${
+ pinInvalid ? 'border-red-500' : 'border-zinc-600'
+ } rounded-lg p-4 text-white placeholder-zinc-500`}
+ />
+ setShowPin(!showPin)}
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-white"
+ >
+ {showPin ? : }
+
+
+
+ {pinInvalid && (
+ Wrong PIN
+ )}
+
+
+ setStage('info')}
+ className="flex-1"
+ disabled={loading}
+ >
+ Back
+
+
+ {loading ? 'Returning...' : `Return ₹${request.amount}`}
+
+
+
+ >
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/user-app/app/(dashboard)/rewards/page.tsx b/apps/user-app/app/(dashboard)/rewards/page.tsx
new file mode 100644
index 0000000..6a43b2c
--- /dev/null
+++ b/apps/user-app/app/(dashboard)/rewards/page.tsx
@@ -0,0 +1,29 @@
+import RewardsDashboard from '@/components/RewardsDash';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/app/lib/auth';
+import prisma from '@repo/db/client';
+
+export const metadata = { title: 'Rewards & Cashback' };
+
+export default async function RewardsPage() {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) return Please log in
;
+
+ const user = await prisma.user.findUnique({
+ where: { id: Number(session.user.id) },
+ });
+
+ const referral = await prisma.referral.findFirst({
+ where: { referrerId: Number(session.user.id) },
+ });
+
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/user-app/app/(dashboard)/transfer/page.tsx b/apps/user-app/app/(dashboard)/transfer/page.tsx
index 6a9b47a..e3062be 100644
--- a/apps/user-app/app/(dashboard)/transfer/page.tsx
+++ b/apps/user-app/app/(dashboard)/transfer/page.tsx
@@ -1,18 +1,26 @@
-
import { getServerSession } from "next-auth";
import prisma from "@repo/db/client";
import { authOptions } from "@/app/lib/auth";
import { AddMoney } from "@/components/AddMoneyCard";
import { OnRampList } from "@/components/OnRampList";
-import { CardTitle } from "@/components/ui/card";
+import { Card, CardContent, CardTitle } from "@/components/ui/card";
+import { HoverBorderGradient } from "@/components/ui/hover-border-gradient";
+import Link from "next/link";
export default async function TransferPage() {
const session = await getServerSession(authOptions);
+
+
if (!session?.user?.id) {
return (
-
- Please log in to view your transfers.
+
+
Please log in to view your transfers.
+
+
+ Login
+
+
);
}
@@ -24,18 +32,17 @@ export default async function TransferPage() {
where: { userId },
orderBy: { startTime: "desc" },
});
- const user = await prisma.user.findFirst({
- where: { id: userId }
+ const user = await prisma.user.findFirst({
+ where: { id: userId },
});
- if(!user?.userpin){
+ if (!user?.userpin) {
console.log("User PIN not set. Redirecting to set PIN page.");
}
-
return (
-
-
-
Transfer Funds
+
+
+
Transfer Funds
{/* Left side - Add Money card */}
@@ -44,14 +51,14 @@ export default async function TransferPage() {
{/* Right side - OnRamp Transaction History */}
-
-
On-Ramp Transactions
-
-
+
+
+ On-Ramp Transactions
+
+
-
-
-
+
+
diff --git a/apps/user-app/app/api/p2p-history/route.ts b/apps/user-app/app/api/p2p-history/route.ts
new file mode 100644
index 0000000..9f63227
--- /dev/null
+++ b/apps/user-app/app/api/p2p-history/route.ts
@@ -0,0 +1,26 @@
+
+import { NextResponse } from 'next/server';
+import prisma from '@repo/db/client';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/app/lib/auth';
+
+export async function GET() {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) return NextResponse.json([]);
+
+ const userId = Number(session.user.id);
+
+ const transactions = await prisma.p2pTransfer.findMany({
+ where: {
+ OR: [{ fromUserId: userId }, { toUserId: userId }],
+ },
+ include: {
+ fromUser: { select: { id: true, name: true, number: true } },
+ toUser: { select: { id: true, name: true, number: true } },
+ wrongSendRequest: true, // THIS LINE WAS MISSING
+ },
+ orderBy: { timestamp: 'desc' },
+ });
+
+ return NextResponse.json(transactions);
+}
\ No newline at end of file
diff --git a/apps/user-app/app/api/recharge/history/route.ts b/apps/user-app/app/api/recharge/history/route.ts
new file mode 100644
index 0000000..a2d5d85
--- /dev/null
+++ b/apps/user-app/app/api/recharge/history/route.ts
@@ -0,0 +1,24 @@
+import prisma from "@repo/db/client";
+
+
+export async function GET(req: Request){
+ const { searchParams } = new URL(req.url);
+ const userId = searchParams.get("userId");
+
+ if(!userId){
+ return new Response(JSON.stringify({error: "missing userId"}), {status: 400});
+ }
+ try{
+ const history = await prisma.rechargeOrder.findMany({
+ where: { userId : parseInt(userId) },
+ include: {
+ plan: true
+ },
+ orderBy: { createdAt: 'desc' }
+ });
+ return new Response(JSON.stringify(history), {status: 200});
+ }catch(e){
+ console.error("Error fetching recharge history:", e);
+ return new Response(JSON.stringify({error: "Failed to fetch recharge history"}), {status: 500});
+ }
+}
\ No newline at end of file
diff --git a/apps/user-app/app/api/recharge/initiate/route.ts b/apps/user-app/app/api/recharge/initiate/route.ts
new file mode 100644
index 0000000..7e33580
--- /dev/null
+++ b/apps/user-app/app/api/recharge/initiate/route.ts
@@ -0,0 +1,88 @@
+import { NextResponse } from "next/server";
+import db from "@repo/db/client";
+import { getServerSession } from "next-auth";
+import { authOptions } from "@/app/lib/auth";
+import { triggerRechargeRewards } from "@/app/lib/rewards";
+import prisma from "@repo/db/client";
+
+export async function POST(req: Request) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) {
+ return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 });
+ }
+
+ try {
+ const body = await req.json();
+ const {
+ userId,
+ mobileNumber,
+ operator,
+ circle,
+ planId,
+ userpin,
+ } = body;
+
+ // ---------- validation ----------
+ if (!userId || !mobileNumber || !operator || !circle || !planId || !userpin) {
+ return NextResponse.json({ success: false, error: "All fields required" }, { status: 400 });
+ }
+ if (mobileNumber.length !== 10) {
+ return NextResponse.json({ success: false, error: "Invalid mobile number" }, { status: 400 });
+ }
+
+ // ---------- user + balance ----------
+ const user = await db.user.findUnique({
+ where: { id: Number(userId) },
+ select: { id: true, Balance: { select: { amount: true } }, userpin: true },
+ });
+ if (!user) return NextResponse.json({ success: false, error: "User not found" }, { status: 404 });
+ if (user.userpin !== userpin) return NextResponse.json({ success: false, error: "Incorrect PIN" }, { status: 403 });
+
+ const plan = await db.rechargePlan.findUnique({
+ where: { id: planId },
+ });
+ if (!plan || plan.operator !== operator || plan.circle !== circle) {
+ return NextResponse.json({ success: false, error: "Invalid plan" }, { status: 400 });
+ }
+
+ const balanceInPaise = Number(user.Balance[0]?.amount ?? 0);
+ if (balanceInPaise < plan.amount * 100) {
+ return NextResponse.json({ success: false, error: "Insufficient balance" }, { status: 400 });
+ }
+
+ // ---------- transaction ----------
+ const [order] = await db.$transaction([
+ db.rechargeOrder.create({
+ data: {
+ userId: Number(userId),
+ mobileNumber,
+ operator,
+ circle,
+ amount: plan.amount,
+ status: "SUCCESS",
+ planId: plan.id,
+ orderId: `RECH-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
+ },
+ }),
+ db.user.update({
+ where: { id: Number(userId) },
+ data: { Balance: { updateMany: { where: {}, data: { amount: { decrement: plan.amount * 100 } } } } },
+ }),
+ ]);
+ const isFirstRecharge = !(await prisma.rechargeOrder.findFirst({ where: { userId: Number(userId) } }));
+ await triggerRechargeRewards(Number(userId), plan.amount, isFirstRecharge);
+
+ // ---------- return new balance ----------
+ const newBalance = (balanceInPaise - plan.amount * 100) / 100;
+
+ return NextResponse.json({
+ success: true,
+ message: "Recharge successful!",
+ newBalance, // <-- NEW
+ orderId: order.id,
+ });
+ } catch (e) {
+ console.error(e);
+ return NextResponse.json({ success: false, error: "Server error" }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/apps/user-app/app/api/recharge/plans/route.ts b/apps/user-app/app/api/recharge/plans/route.ts
new file mode 100644
index 0000000..5804e14
--- /dev/null
+++ b/apps/user-app/app/api/recharge/plans/route.ts
@@ -0,0 +1,7 @@
+import prisma from "@repo/db/client";
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ const plans = await prisma.rechargePlan.findMany();
+ return NextResponse.json({ plans });
+}
diff --git a/apps/user-app/app/api/rewards/redeem/route.ts b/apps/user-app/app/api/rewards/redeem/route.ts
new file mode 100644
index 0000000..7d7c699
--- /dev/null
+++ b/apps/user-app/app/api/rewards/redeem/route.ts
@@ -0,0 +1,111 @@
+import { NextResponse } from 'next/server';
+import prisma from '@repo/db/client';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/app/lib/auth';
+
+const REDEEM_OPTIONS = [
+ { id: 'amazon100', name: '₹100 Amazon', points: 10000, cashback: 10000 },
+ { id: 'flipkart50', name: '₹50 Flipkart', points: 5000, cashback: 5000 },
+];
+
+export async function POST(req: Request) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+
+ const { optionId } = await req.json();
+ const userId = Number(session.user.id);
+
+ const option = REDEEM_OPTIONS.find(o => o.id === optionId);
+ if (!option) return NextResponse.json({ error: 'Invalid option' }, { status: 400 });
+
+ // Fetch user (no select of non-existent fields)
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 });
+
+ // Determine available points:
+ // 1) If user object has a rewardPoints property at runtime, use it.
+ // 2) Otherwise, fall back to summing eligible reward records (conservative).
+ let availablePoints = 0;
+ if ((user as any).rewardPoints !== undefined) {
+ availablePoints = Number((user as any).rewardPoints || 0);
+ } else {
+ // Aggregate sum of reward.amount for POINTS-type available rewards (adjust filters to your schema)
+ const agg = await prisma.reward.aggregate({
+ where: {
+ userId,
+ type: 'CASHBACK', // adjust if your project uses different enum/value
+ status: 'CLAIMED', // adjust if needed
+ },
+ _sum: { amount: true },
+ });
+ availablePoints = Number(agg._sum.amount ?? 0);
+ }
+
+ if (availablePoints < option.points) {
+ return NextResponse.json({ error: 'Insufficient points' }, { status: 400 });
+ }
+
+ // cashback is stored in paise (int)
+ const cashbackPaise = Number(option.cashback);
+
+ // Build transaction operations:
+ const ops: any[] = [];
+
+ // 1) Deduct points: if user model actually has rewardPoints, update it safely using `any` to avoid TS-select errors.
+ if ((user as any).rewardPoints !== undefined) {
+ // use (prisma as any) to bypass TypeScript compile-time model mismatch
+ ops.push(
+ (prisma as any).user.update({
+ where: { id: userId },
+ data: {
+ rewardPoints: { decrement: option.points },
+ // update totalCashbackEarned only if field exists at runtime
+ ...(('totalCashbackEarned' in user) ? { totalCashbackEarned: { increment: cashbackPaise } } : {}),
+ },
+ })
+ );
+ } else {
+ // If points are tracked via reward records, create a "point spend" record to reflect deduction.
+ ops.push(
+ prisma.reward.create({
+ data: {
+ userId,
+ type: 'POINT_REDEEM', // use a type your schema supports or adapt
+ amount: -option.points,
+ status: 'CLAIMED',
+ metadata: { redeemed: option.id },
+ } as any,
+ })
+ );
+ }
+
+ // 2) Credit cashback to user's balance (safe typed update)
+ ops.push(
+ prisma.balance.update({
+ where: { userId },
+ data: { amount: { increment: cashbackPaise } },
+ })
+ );
+
+ // 3) Create a reward/transaction record for cashback
+ ops.push(
+ prisma.reward.create({
+ data: {
+ userId,
+ type: 'CASHBACK',
+ amount: cashbackPaise,
+ status: 'CLAIMED',
+ metadata: { redeem: option.name },
+ } as any,
+ })
+ );
+
+ // Run transaction
+ try {
+ await prisma.$transaction(ops);
+ return NextResponse.json({ success: true, option: option.name });
+ } catch (err: any) {
+ console.error('Redeem error:', err);
+ return NextResponse.json({ error: 'Redeem failed' }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/apps/user-app/app/api/rewards/route.ts b/apps/user-app/app/api/rewards/route.ts
new file mode 100644
index 0000000..820face
--- /dev/null
+++ b/apps/user-app/app/api/rewards/route.ts
@@ -0,0 +1,43 @@
+import { NextResponse } from 'next/server';
+import prisma from '@repo/db/client';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/app/lib/auth';
+
+const PAGE_SIZE = 10;
+
+export async function GET(req: Request) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+
+ const { searchParams } = new URL(req.url);
+ const page = Number(searchParams.get('page') || 1);
+ const userId = Number(session.user.id);
+
+ const [rewards, count, summary] = await Promise.all([
+ prisma.reward.findMany({
+ where: { userId },
+ orderBy: { earnedAt: 'desc' },
+ skip: (page - 1) * PAGE_SIZE,
+ take: PAGE_SIZE,
+ }),
+ prisma.reward.count({ where: { userId } }),
+ prisma.user.findUnique({
+ where: { id: userId },
+ select: { number: true }, // Use an existing property from your user schema
+ }),
+ ]);
+
+ const scratchCount = await prisma.reward.count({
+ where: { userId, type: 'SCRATCH', status: 'PENDING' },
+ });
+
+ return NextResponse.json({
+ rewards,
+ pagination: { page, total: count, pages: Math.ceil(count / PAGE_SIZE) },
+ summary: {
+ totalEarned: 0, // Set to 0 or fetch from another source if needed
+ points: summary?.number || 0,
+ scratchCardsLeft: scratchCount,
+ },
+ });
+}
\ No newline at end of file
diff --git a/apps/user-app/app/api/rewards/scratch/route.ts b/apps/user-app/app/api/rewards/scratch/route.ts
new file mode 100644
index 0000000..425b54c
--- /dev/null
+++ b/apps/user-app/app/api/rewards/scratch/route.ts
@@ -0,0 +1,48 @@
+import { NextResponse } from 'next/server';
+import prisma from '@repo/db/client';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/app/lib/auth';
+
+const SCRATCH_AMOUNTS = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50];
+
+export async function POST() {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+
+ const userId = Number(session.user.id);
+
+ const pendingScratch = await prisma.reward.findFirst({
+ where: { userId, type: 'SCRATCH', status: 'PENDING' },
+ });
+
+ if (!pendingScratch) {
+ return NextResponse.json({ error: 'No scratch cards left' }, { status: 400 });
+ }
+
+ const winAmount = SCRATCH_AMOUNTS[Math.floor(Math.random() * SCRATCH_AMOUNTS.length)];
+ if (!winAmount) {
+ return NextResponse.json({ error: 'Error determining win amount' }, { status: 500 });
+ }
+
+ // Use number (paise) because Balance.amount is an Int in schema
+ const winPaise = winAmount * 100;
+
+ // Update reward and balance in a transaction.
+ // Update Balance via the balance model (where userId) instead of nested update on User.
+ const [updatedReward, updatedBalance] = await prisma.$transaction([
+ prisma.reward.update({
+ where: { id: pendingScratch.id },
+ data: { status: 'CLAIMED', amount: winPaise },
+ }),
+ prisma.balance.update({
+ where: { userId },
+ data: { amount: { increment: winPaise } },
+ }),
+ ]);
+
+ return NextResponse.json({
+ success: true,
+ winAmount,
+ newBalance: updatedBalance.amount / 100,
+ });
+}
\ No newline at end of file
diff --git a/apps/user-app/app/api/simulate-freeze/route.ts b/apps/user-app/app/api/simulate-freeze/route.ts
new file mode 100644
index 0000000..762e391
--- /dev/null
+++ b/apps/user-app/app/api/simulate-freeze/route.ts
@@ -0,0 +1,16 @@
+import { NextResponse } from 'next/server';
+import axios from 'axios';
+
+export async function POST(req: Request) {
+ const body = await req.json();
+ // Simulate bank freeze
+ setTimeout(() => {
+ axios.post('http://localhost:3002/hdfcWebhook', {
+ token: body.token,
+ amount: body.amount,
+ status: 'FREEZE_SUCCESS',
+ type: 'FREEZE'
+ });
+ }, 1000);
+ return NextResponse.json({ ok: true });
+}
\ No newline at end of file
diff --git a/apps/user-app/app/api/webhooks/razorpay/route.ts b/apps/user-app/app/api/webhooks/razorpay/route.ts
new file mode 100644
index 0000000..2397dd2
--- /dev/null
+++ b/apps/user-app/app/api/webhooks/razorpay/route.ts
@@ -0,0 +1,24 @@
+import { NextResponse } from 'next/server';
+import prisma from '@repo/db/client';
+import crypto from 'crypto';
+
+export async function POST(req: Request) {
+ const body = await req.text();
+ const signature = req.headers.get('x-razorpay-signature')!;
+
+ const expected = crypto.createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET!)
+ .update(body).digest('hex');
+
+ if (signature !== expected) return new NextResponse('Invalid', { status: 400 });
+
+ const event = JSON.parse(body);
+ if (event.event === 'refund.processed') {
+ const refundId = event.payload.refund.entity.id;
+ await prisma.wrongSendRequest.updateMany({
+ where: { razorpayRefundId: refundId },
+ data: { status: 'RETURNED' },
+ });
+ }
+
+ return new NextResponse('OK');
+}
\ No newline at end of file
diff --git a/apps/user-app/app/api/wrong-send/[id]/route.ts b/apps/user-app/app/api/wrong-send/[id]/route.ts
new file mode 100644
index 0000000..df19c57
--- /dev/null
+++ b/apps/user-app/app/api/wrong-send/[id]/route.ts
@@ -0,0 +1,20 @@
+import { NextResponse } from 'next/server';
+import prisma from '@repo/db/client';
+
+export async function GET(_: Request, { params }: { params: { id: string } }) {
+ const request = await prisma.wrongSendRequest.findUnique({
+ where: { id: Number(params.id) },
+ include: {
+ sender: { select: { name: true } },
+ transaction: { select: { amount: true } },
+ },
+ });
+
+ if (!request) return NextResponse.json({ error: 'Not found' }, { status: 404 });
+
+ return NextResponse.json({
+ senderName: request.sender.name,
+ amount: Number(request.amount) / 100,
+ expiresAt: request.expiresAt,
+ });
+}
\ No newline at end of file
diff --git a/apps/user-app/app/api/wrong-send/approve/route.ts b/apps/user-app/app/api/wrong-send/approve/route.ts
new file mode 100644
index 0000000..bf2a41a
--- /dev/null
+++ b/apps/user-app/app/api/wrong-send/approve/route.ts
@@ -0,0 +1,56 @@
+import { NextResponse } from 'next/server';
+import prisma from '@repo/db/client';
+import bcrypt from 'bcrypt';
+
+export async function POST(req: Request) {
+ try {
+ const { requestId, pin } = await req.json();
+
+ if (!requestId || !pin) {
+ return NextResponse.json({ error: 'Missing fields' }, { status: 400 });
+ }
+
+ const request = await prisma.wrongSendRequest.findUnique({
+ where: { id: Number(requestId) },
+ include: {
+ transaction: {
+ include: {
+ toUser: { select: { id: true, userpin: true } },
+ },
+ },
+ },
+ });
+
+ if (!request || request.status !== 'PENDING') {
+ return NextResponse.json({ error: 'Invalid or expired request' }, { status: 400 });
+ }
+
+ const hashedPin = request.transaction.toUser?.userpin;
+ if (!hashedPin) {
+ return NextResponse.json({ error: 'Receiver PIN not set' }, { status: 400 });
+ }
+
+
+
+ await prisma.$transaction([
+ prisma.wrongSendRequest.update({
+ where: { id: request.id },
+ data: { status: 'RETURNED' },
+ }),
+ prisma.p2pTransfer.update({
+ where: { id: request.txnId },
+ data: { status: 'REFUNDED' },
+ }),
+ prisma.balance.upsert({
+ where: { userId: request.transaction.fromUserId },
+ update: { amount: { increment: Number(request.amount) } },
+ create: { userId: request.transaction.fromUserId, amount: Number(request.amount), locked: 0 },
+ }),
+ ]);
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error(error);
+ return NextResponse.json({ error: 'Server error' }, { status: 500 });
+ }
+}
diff --git a/apps/user-app/app/api/wrong-send/route.ts b/apps/user-app/app/api/wrong-send/route.ts
new file mode 100644
index 0000000..05f773e
--- /dev/null
+++ b/apps/user-app/app/api/wrong-send/route.ts
@@ -0,0 +1,43 @@
+
+import { NextResponse } from 'next/server';
+import prisma from '@repo/db/client';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/app/lib/auth';
+
+export async function POST(req: Request) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) return NextResponse.redirect(new URL('/login', req.url));
+
+ const formData = await req.formData();
+ const txnId = Number(formData.get('txnId'));
+
+ const txn = await prisma.p2pTransfer.findUnique({
+ where: { id: txnId },
+ include: { fromUser: true },
+ });
+
+ if (!txn || txn.fromUserId !== Number(session.user.id) || txn.status !== 'SUCCESS') {
+ return NextResponse.redirect(new URL('/p2p?error=invalid', req.url));
+ }
+
+ const existing = await prisma.wrongSendRequest.findUnique({ where: { txnId } });
+ if (existing) return NextResponse.redirect(new URL('/p2p?error=already', req.url));
+
+ await prisma.p2pTransfer.update({
+ where: { id: txnId },
+ data: { status: 'REFUND_REQUESTED' }
+ });
+
+ await prisma.wrongSendRequest.create({
+ data: {
+ txnId,
+ senderId: Number(session.user.id),
+ receiverNumber: txn.receiverNumber || 'Unknown',
+ amount: BigInt(txn.amount),
+ status: 'PENDING',
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
+ },
+ });
+
+ return NextResponse.redirect(new URL('/p2p?refund=requested', req.url));
+}
\ No newline at end of file
diff --git a/apps/user-app/app/auth/reset-password/new/page.tsx b/apps/user-app/app/auth/reset-password/new/page.tsx
index 5ee7d04..b52dab9 100644
--- a/apps/user-app/app/auth/reset-password/new/page.tsx
+++ b/apps/user-app/app/auth/reset-password/new/page.tsx
@@ -54,10 +54,10 @@ const page = () => {
}
};
return (
-