Dev#147
Conversation
There was a problem hiding this comment.
Code Review
This pull request integrates Cashfree payments into the billing system, adds a payment architecture guide, and refactors webhook routing and various UI components. Critical feedback highlights a security regression in OrgRoute.tsx where role-based protection for webhooks was removed, and several bugs in QuickRecharge.tsx where swallowed errors could falsely credit wallets and stale closures exist due to empty dependency arrays. Additionally, reviewers noted hardcoded customer details, a redundant base URL in the payment hook, and a potential race condition when initializing the Cashfree instance.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const handlePayment = useCallback(async (Data: handlePayType) => { | ||
| try { | ||
| console.log("Payment Data echo echo:--->", Data); | ||
| const cashfree = await getCashfree(); | ||
|
|
||
| const result = await cashfree.checkout({ | ||
| paymentSessionId: Data.session_id, | ||
|
|
||
| redirectTarget: "_modal", | ||
| }); | ||
|
|
||
| if (result.error) { | ||
| throw new Error(result.error.message); | ||
| } | ||
| } catch (error) { | ||
| console.error("Payment failed:", error); | ||
| addToast("error", "Payment Failed", "An error occurred during payment. Please try again."); | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
The handlePayment function catches errors but does not rethrow them. This causes the calling function createPayment (and subsequently handleRecharge) to assume the payment succeeded, proceeding to credit the user's wallet even if the payment failed or was cancelled. Additionally, addToast is missing from the useCallback dependency array.
| const handlePayment = useCallback(async (Data: handlePayType) => { | |
| try { | |
| console.log("Payment Data echo echo:--->", Data); | |
| const cashfree = await getCashfree(); | |
| const result = await cashfree.checkout({ | |
| paymentSessionId: Data.session_id, | |
| redirectTarget: "_modal", | |
| }); | |
| if (result.error) { | |
| throw new Error(result.error.message); | |
| } | |
| } catch (error) { | |
| console.error("Payment failed:", error); | |
| addToast("error", "Payment Failed", "An error occurred during payment. Please try again."); | |
| } | |
| }, []); | |
| const handlePayment = useCallback(async (Data: handlePayType) => { | |
| try { | |
| console.log("Payment Data echo echo:--->", Data); | |
| const cashfree = await getCashfree(); | |
| const result = await cashfree.checkout({ | |
| paymentSessionId: Data.session_id, | |
| redirectTarget: "_modal", | |
| }); | |
| if (result.error) { | |
| throw new Error(result.error.message); | |
| } | |
| } catch (error) { | |
| console.error("Payment failed:", error); | |
| addToast("error", "Payment Failed", "An error occurred during payment. Please try again."); | |
| throw error; | |
| } | |
| }, [addToast]); |
| <Route path="webhooks/create" element={<CreateWebhookPage />} /> | ||
| <Route path="webhooks/:id" element={<WebhookDetailsPage />} /> | ||
| <Route path="webhooks/:id/edit" element={<EditWebhookPage />} /> | ||
| <Route path="webhooks/:id/logs" element={<WebhookLogsPage />} /> |
There was a problem hiding this comment.
This change introduces two major issues:
- Security Regression: The
ProtectedRoutewrapper restricting access to webhook routes based on roles (CommunityOwner,Admin,Organizer) has been completely removed. This allows unauthorized users to view, create, edit, and access logs for webhooks. - Missing Route: The route for
WebhookListPage(the main webhooks list view) has been omitted in the refactored routes, even though the component is still imported.
Please restore the role-based protection and the list route. Note that you will also need to restore the import of ProtectedRoute from ./ProtectedRoute on line 16.
| <Route path="webhooks/create" element={<CreateWebhookPage />} /> | |
| <Route path="webhooks/:id" element={<WebhookDetailsPage />} /> | |
| <Route path="webhooks/:id/edit" element={<EditWebhookPage />} /> | |
| <Route path="webhooks/:id/logs" element={<WebhookLogsPage />} /> | |
| <Route | |
| path="webhooks/*" | |
| element={ | |
| <ProtectedRoute allowedRoles={["CommunityOwner", "Admin", "Organizer"]}> | |
| <Routes> | |
| <Route index element={<WebhookListPage />} /> | |
| <Route path="create" element={<CreateWebhookPage />} /> | |
| <Route path=":id" element={<WebhookDetailsPage />} /> | |
| <Route path=":id/edit" element={<EditWebhookPage />} /> | |
| <Route path=":id/logs" element={<WebhookLogsPage />} /> | |
| </Routes> | |
| </ProtectedRoute> | |
| } | |
| /> |
| let createPayment = useCallback(async () => { | ||
| try { | ||
| let res = await createPaymentIntent.mutateAsync({ | ||
| customerEmail: "test@gmail.com", | ||
| customerName: "Test User", | ||
| customerPhone: "9999999999", | ||
| order_amount: Number(amountStr), // Convert to paise | ||
| }); | ||
|
|
||
| console.log("Payment Intent Created -->", res.data.order_id, res.data.payment_session_id); | ||
|
|
||
| console.log("BEFORE HANDLE PAYMENT"); | ||
|
|
||
| await handlePayment({ | ||
| orderId: res.data.order_id, | ||
| session_id: res.data.payment_session_id, | ||
| }); | ||
|
|
||
| console.log("AFTER HANDLE PAYMENT"); | ||
| } catch (error) { | ||
| console.error("Error creating payment intent:", error); | ||
| addToast("error", "Payment Failed", "Unable to create payment intent."); | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
The createPayment function has several issues:
- It catches errors from
createPaymentIntent.mutateAsyncandhandlePaymentbut does not rethrow them, allowinghandleRechargeto proceed with crediting the wallet even on failure. - The dependency array for
useCallbackis empty[], which creates a stale closure bug because it referencesamountStr,createPaymentIntent,handlePayment, andaddToast. - Customer details are hardcoded (
test@gmail.com,Test User,9999999999). These should be dynamically retrieved from the active user session or profile context.
const createPayment = useCallback(async () => {
try {
// TODO: Retrieve actual customer details from user session/context
const res = await createPaymentIntent.mutateAsync({
customerEmail: "test@gmail.com",
customerName: "Test User",
customerPhone: "9999999999",
order_amount: Number(amountStr),
});
console.log("Payment Intent Created -->", res.data.order_id, res.data.payment_session_id);
await handlePayment({
orderId: res.data.order_id,
session_id: res.data.payment_session_id,
});
} catch (error) {
console.error("Error creating payment intent:", error);
addToast("error", "Payment Failed", "Unable to create payment intent.");
throw error;
}
}, [amountStr, createPaymentIntent, handlePayment, addToast]);
| const baseUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000/api/v1"; | ||
|
|
||
| export const useCreatePaymentIntent = () => { | ||
| return useMutation({ | ||
| mutationKey: ["createPaymentIntent"], | ||
|
|
||
| mutationFn: async (paymentData: { | ||
| customerEmail: string; | ||
| customerName: string; | ||
| customerPhone: string; | ||
| order_amount: number; | ||
| }) => { | ||
| const response = await api.post(`${baseUrl}/payment/create-payment-intent`, paymentData); | ||
| return response.data; | ||
| }, |
There was a problem hiding this comment.
If the shared api axios instance (imported from @/utils/axios.utils) is already configured with a baseURL, prepending ${baseUrl} manually is redundant and can lead to incorrect request URLs (e.g., double base URLs). It is cleaner and more robust to use relative paths like /payment/create-payment-intent and let the axios instance handle the base URL configuration.
export const useCreatePaymentIntent = () => {
return useMutation({
mutationKey: ["createPaymentIntent"],
mutationFn: async (paymentData: {
customerEmail: string;
customerName: string;
customerPhone: string;
order_amount: number;
}) => {
const response = await api.post("/payment/create-payment-intent", paymentData);
return response.data;
},| let cashfreeInstance: Cashfree | null = null; | ||
|
|
||
| export async function getCashfree(): Promise<Cashfree> { | ||
| if (cashfreeInstance) { | ||
| return cashfreeInstance; | ||
| } | ||
|
|
||
| cashfreeInstance = await load({ | ||
| mode: import.meta.env.VITE_CASHFREE_ENV === "production" ? "production" : "sandbox", | ||
|
|
||
| }); | ||
|
|
||
| return cashfreeInstance; | ||
| } |
There was a problem hiding this comment.
If getCashfree() is called concurrently before the first load promise resolves, cashfreeInstance will still be null, leading to multiple redundant calls to load(). Storing the initialization promise instead of the resolved instance avoids this race condition.
let cashfreePromise: Promise<Cashfree> | null = null;
export function getCashfree(): Promise<Cashfree> {
if (!cashfreePromise) {
cashfreePromise = load({
mode: import.meta.env.VITE_CASHFREE_ENV === "production" ? "production" : "sandbox",
});
}
return cashfreePromise;
}
No description provided.