diff --git a/.env.example b/.env.example index 204439a..c616002 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,6 @@ TAURI_SIGNING_PRIVATE_KEY= # Optional for local release tooling that talks to GitHub API. # GITHUB_TOKEN= + + +VITE_CASHFREE_ENV \ No newline at end of file diff --git a/docs/Cashfree_Whitelisting_strategy.md b/docs/Cashfree_Whitelisting_strategy.md new file mode 100644 index 0000000..6353fa8 --- /dev/null +++ b/docs/Cashfree_Whitelisting_strategy.md @@ -0,0 +1,613 @@ +# CommDesk Cashfree Production Whitelisting & Desktop Payment Architecture Guide + +# Introduction + +This document defines the complete payment security and whitelisting strategy for CommDesk. + +It explains: + +* Why Cashfree requires whitelisting +* Why localhost is problematic +* How production desktop applications should handle payments +* Security implications +* Tauri-specific considerations +* Recommended architecture +* Scaling considerations +* Future gateway support + +This document serves as the official payment integration reference for CommDesk. + +--- + +# What Is Whitelisting? + +Whitelisting is a security mechanism used by payment providers to ensure that payment requests originate only from trusted applications. + +When a payment checkout is opened, Cashfree validates: + +* Origin +* Domain +* Application Identity +* Merchant Configuration + +Before allowing users to proceed. + +Example: + +Allowed: + +https://app.commdesk.in + +Rejected: + +http://localhost:1420 + +Unknown domains + +Unregistered applications + +--- + +# Why Cashfree Uses Whitelisting + +Payment providers process real money. + +Without whitelisting, attackers could: + +* Clone merchant websites +* Spoof payment pages +* Create fake checkout flows +* Redirect users to malicious applications +* Harvest customer payment information + +Whitelisting prevents these attacks. + +--- + +# Security Threats Without Whitelisting + +## Fake Frontend Attack + +Attacker creates: + +https://commdesk-fake.xyz + +Copies CommDesk UI. + +Attempts: + +```txt +Create Cashfree Checkout +Collect Payments +Pretend To Be CommDesk +``` + +Without origin validation: + +Users could unknowingly pay attackers. + +--- + +## Session Hijacking + +Attacker steals: + +```json +{ + "paymentSessionId": "session_xxx" +} +``` + +Attempts to launch checkout from another application. + +Whitelisting prevents unauthorized origins. + +--- + +## Desktop Application Spoofing + +Attacker creates: + +```txt +CommDesk Desktop.exe +``` + +Fake application. + +Attempts: + +```txt +Use CommDesk Merchant Credentials +Collect User Payments +``` + +App identity validation reduces this risk. + +--- + +# Why Localhost Is Usually Rejected + +Localhost: + +```txt +http://localhost:1420 +http://localhost:3000 +http://127.0.0.1 +``` + +Cannot be uniquely owned. + +Every computer on Earth has localhost. + +Cashfree cannot verify: + +* Ownership +* Authenticity +* Merchant Control + +Because of this: + +Production systems generally reject localhost. + +--- + +# Sandbox vs Production + +## Sandbox + +Purpose: + +Testing + +Allowed: + +* Localhost +* Temporary Domains +* Development Machines + +Typical Usage: + +```txt +localhost:1420 +localhost:5173 +``` + +--- + +## Production + +Purpose: + +Real Transactions + +Requirements: + +* Verified Ownership +* Secure Domains +* Merchant Approval + +Expected: + +```txt +https://app.commdesk.in +``` + +Not: + +```txt +http://localhost:1420 +``` + +--- + +# CommDesk Architecture + +## Frontend + +Technology: + +* React +* TypeScript +* Tauri + +Responsibilities: + +* Create Payment Intent +* Open Checkout +* Display Status + +Never: + +* Credit Wallet +* Mark Success +* Process Refunds + +--- + +## Backend + +Technology: + +* Node.js +* Express +* PostgreSQL +* Redis +* RabbitMQ + +Responsibilities: + +* Create Orders +* Verify Payments +* Process Webhooks +* Generate Invoices +* Credit Wallets + +Backend is the source of truth. + +--- + +# Tauri Desktop Application Challenge + +Tauri applications run locally. + +Example: + +```txt +http://localhost:1420 +``` + +during development. + +Production users may run: + +```txt +CommDesk.exe +``` + +without a public website. + +This creates challenges for payment providers because: + +```txt +No public domain +No browser origin +No ownership verification +``` + +--- + +# Possible Production Strategies + +## Strategy A + +### Domain Whitelisting + +Whitelist: + +```txt +https://app.commdesk.in +``` + +Benefits: + +* Simple +* Official +* Easily Approved + +Recommended. + +--- + +## Strategy B + +### Bundle Identifier Approval + +Tauri Identifier: + +```txt +com.commdesk.desktop +``` + +Request approval from Cashfree. + +Benefits: + +Native desktop experience. + +Limitations: + +Requires manual approval. + +--- + +## Strategy C + +### Hosted Payment Window + +Flow: + +```txt +Desktop App +↓ +Create Order +↓ +Open Payment Window +↓ +Cashfree Hosted Checkout +↓ +Webhook +↓ +Backend Verification +``` + +Benefits: + +* Reliable +* Scalable +* Cross-platform + +Recommended fallback. + +--- + +# Why Whitelisting Matters For CommDesk + +Without proper whitelisting: + +Users experience: + +```txt +Payment Session Invalid +Origin Not Approved +Checkout Refused +``` + +These issues become production outages. + +Proper whitelisting prevents: + +* Checkout failures +* Fraud +* Unauthorized integrations +* Merchant account risks + +--- + +# CommDesk Production Requirements + +## Domains + +Frontend + +https://app.commdesk.in + +Backend + +https://api.commdesk.in + +Website + +https://commdesk.in + +--- + +## Desktop Identity + +Bundle Identifier: + +```txt +com.commdesk.desktop +``` + +Platforms: + +* Windows +* Linux +* macOS + +--- + +# Whitelisting Request Template + +Merchant Name: + +CommDesk + +Application Type: + +Desktop Application + +Framework: + +Tauri + +Website: + +https://commdesk.in + +Frontend: + +https://app.commdesk.in + +Backend: + +https://api.commdesk.in + +Bundle Identifier: + +com.commdesk.desktop + +Platforms: + +Windows +Linux +macOS + +Requirement: + +Need Cashfree Checkout support for CommDesk Desktop Application. + +--- + +# Recommended Payment Flow + +User +↓ +Create Intent +↓ +Backend Creates Order +↓ +Store Payment(PENDING) +↓ +Return Session +↓ +Open Checkout +↓ +User Pays +↓ +Cashfree Webhook +↓ +Verify Signature +↓ +Store Audit Log +↓ +Publish RabbitMQ Event +↓ +Payment Success Consumer +↓ +Credit Wallet +↓ +Create Ledger Entry +↓ +Generate Invoice +↓ +Send Notification +↓ +Update Dashboard + +--- + +# Why Frontend Must Never Be Trusted + +Bad: + +```txt +Frontend +↓ +Payment Success +↓ +Credit Wallet +``` + +User can manipulate frontend. + +Result: + +Financial fraud. + +--- + +Good: + +```txt +Frontend +↓ +Payment Success +↓ +Wait For Webhook + +Webhook +↓ +Verify Signature +↓ +Credit Wallet +``` + +Only verified provider events modify money. + +--- + +# Multi-Gateway Future + +The same architecture supports: + +* Cashfree +* Razorpay +* Stripe +* PayPal +* PhonePe +* Juspay + +without redesign. + +--- + +# Enterprise Scaling + +Supports: + +* Wallet Top-ups +* Membership Fees +* Event Registrations +* Sponsorship Contributions +* Subscription Billing +* GST Invoicing +* Multi-Tenant Communities + +For: + +* Colleges +* Universities +* Clubs +* NGOs +* Enterprises + +--- + +# Compliance + +Maintain: + +* Audit Logs +* Payment Logs +* Refund Logs +* Invoice Logs +* Webhook Logs + +Retention: + +Minimum 7 Years + +Never Delete Financial Records. + +--- + +# Final Recommendation For CommDesk + +Use: + +Primary: + +https://app.commdesk.in + +Secondary: + +com.commdesk.desktop + +Fallback: + +Hosted Checkout Window + +Never rely on localhost for production. + +Treat localhost only as a development environment. + +Production source of truth: + +Verified Cashfree Webhook + Backend Verification. + +Never trust browser redirects. +Never trust frontend callbacks. +Never trust success pages. + +Trust only verified payment provider events. diff --git a/package.json b/package.json index 0b00702..2f6bbf4 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "test:watch": "vitest" }, "dependencies": { + "@cashfreepayments/cashfree-js": "^1.0.7", + "@cashfreepayments/pg-react": "^1.0.3", "@hookform/resolvers": "^5.2.2", "@tanstack/react-query": "^5.100.9", "@tauri-apps/api": "2.10.1", @@ -25,6 +27,7 @@ "@tauri-apps/plugin-process": "2.3.1", "@tauri-apps/plugin-updater": "2.10.1", "axios": "^1.16.1", + "cashfree-pg": "^6.0.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 399d20e..2a5fb80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@cashfreepayments/cashfree-js': + specifier: ^1.0.7 + version: 1.0.7 + '@cashfreepayments/pg-react': + specifier: ^1.0.3 + version: 1.0.3(@types/react@19.2.14)(redux@5.0.1) '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.75.0(react@19.2.6)) @@ -29,6 +35,9 @@ importers: axios: specifier: ^1.16.1 version: 1.16.1 + cashfree-pg: + specifier: ^6.0.4 + version: 6.0.4 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -161,7 +170,7 @@ importers: version: 7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0) vitest: specifier: ^4.1.6 - version: 4.1.6(@types/node@25.6.2)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.5(@types/node@25.6.2)(typescript@5.8.3))(vite@7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)) + version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.6.2)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.5(@types/node@25.6.2)(typescript@5.8.3))(vite@7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)) packages: @@ -336,6 +345,12 @@ packages: resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true + '@cashfreepayments/cashfree-js@1.0.7': + resolution: {integrity: sha512-WRhjGdUgkPdWbq0YoMsi6hRqyT4XTIGUMNnATQsfz0GIDFP2sjtVon/kcihDVtO6pzEZrjded62Do9xK2VkjPQ==} + + '@cashfreepayments/pg-react@1.0.3': + resolution: {integrity: sha512-qOT27MEA5GwQVC703aefUpEkyJjV7K/lML8jOmnu46HqarPOirFFgraKudoCQ8BOePkSwTBkgYkllsitC/ymyA==} + '@csstools/color-helpers@6.0.2': resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} engines: {node: '>=20.19.0'} @@ -735,6 +750,42 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.214.0': + resolution: {integrity: sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.7.1': + resolution: {integrity: sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation@0.214.0': + resolution: {integrity: sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.7.1': + resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.7.1': + resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} + '@playwright/test@1.60.0': resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} engines: {node: '>=18'} @@ -1585,6 +1636,51 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@sentry-internal/server-utils@10.57.0': + resolution: {integrity: sha512-Qu8ETmX/ITzteG7Im46b9HOxKKzeaIeqNvftaIlFURu1RUQdHbtGerS7QOmXzwnhuqNGNeiCQYkduB798IfRqA==} + engines: {node: '>=18'} + + '@sentry/core@10.57.0': + resolution: {integrity: sha512-kntItTA2kiT0YpL7encXaF6mkdZMB+y48lwj8w1wkfBpfJAC7sifdgrzLQZqmsqVNE3crg9VfufaAGA+78uFMg==} + engines: {node: '>=18'} + + '@sentry/node-core@10.57.0': + resolution: {integrity: sha512-2v2IF6MfTiu7pimWEq2rYhZsmlwyNbs3bHUsrYFPeP/Rpa6ObDuUWPdVEzJjfyK+AqqYZYxZdV0l3+B13kTEmQ==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/semantic-conventions': ^1.39.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/core': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true + '@opentelemetry/instrumentation': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + '@opentelemetry/semantic-conventions': + optional: true + + '@sentry/node@10.57.0': + resolution: {integrity: sha512-7KEStrJ97wPf1fA5nU5ONeTTcIIlh7oT8OMffEVA1PXmlhFoXhcQZVzr4rM+zj9tfMWT01og5Ng/Grgh3dN+FA==} + engines: {node: '>=18'} + + '@sentry/opentelemetry@10.57.0': + resolution: {integrity: sha512-iwRz8cEK0GOISG34aJRO8GdYOk3nfpuT6dT2GDQrxw8f7JjkJKx9LPU8MaenOFa4MhY+Z02hI6NNcrbsoI3cXg==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/semantic-conventions': ^1.39.0 + '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} @@ -2011,6 +2107,11 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2088,6 +2189,9 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} + axios@1.16.1: resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} @@ -2149,6 +2253,9 @@ packages: caniuse-lite@1.0.30001792: resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + cashfree-pg@6.0.4: + resolution: {integrity: sha512-ioi18y1/6LLzQhmZOWIRBMz3O5iLFTMTCnC0H7ICVFIeEyclDQQZX6Mp/cyXGpQQKXm4yVLm6BiJdOXNM9S/ig==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -2161,6 +2268,9 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -2836,6 +2946,10 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-in-the-middle@3.0.2: + resolution: {integrity: sha512-LGLYRl0A2gtyUJb2WDliBHmk6TtlHwdDjxonacZ8QrEs/ZW+YDgNv2QAfjRQWpS8HqvNcq6GGnN6jrOa5FysDQ==} + engines: {node: '>=18'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -3202,6 +3316,9 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3585,6 +3702,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -4369,6 +4490,19 @@ snapshots: dependencies: css-tree: 3.2.1 + '@cashfreepayments/cashfree-js@1.0.7': {} + + '@cashfreepayments/pg-react@1.0.3(@types/react@19.2.14)(redux@5.0.1)': + dependencies: + '@cashfreepayments/cashfree-js': 1.0.7 + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1) + transitivePeerDependencies: + - '@types/react' + - redux + '@csstools/color-helpers@6.0.2': {} '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': @@ -4690,6 +4824,41 @@ snapshots: '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.214.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.214.0 + import-in-the-middle: 3.0.2 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/semantic-conventions@1.41.1': {} + '@playwright/test@1.60.0': dependencies: playwright: 1.60.0 @@ -5532,6 +5701,48 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@sentry-internal/server-utils@10.57.0': + dependencies: + '@sentry/core': 10.57.0 + + '@sentry/core@10.57.0': {} + + '@sentry/node-core@10.57.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)': + dependencies: + '@sentry/core': 10.57.0 + '@sentry/opentelemetry': 10.57.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) + import-in-the-middle: 3.0.2 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@sentry/node@10.57.0': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@sentry-internal/server-utils': 10.57.0 + '@sentry/core': 10.57.0 + '@sentry/node-core': 10.57.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) + '@sentry/opentelemetry': 10.57.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) + import-in-the-middle: 3.0.2 + transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' + - supports-color + + '@sentry/opentelemetry@10.57.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@sentry/core': 10.57.0 + '@sindresorhus/merge-streams@4.0.0': {} '@standard-schema/spec@1.1.0': {} @@ -5964,6 +6175,10 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -6028,6 +6243,14 @@ snapshots: asynckit@0.4.0: {} + axios@1.15.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + axios@1.16.1: dependencies: follow-redirects: 1.16.0 @@ -6103,6 +6326,15 @@ snapshots: caniuse-lite@1.0.30001792: {} + cashfree-pg@6.0.4: + dependencies: + '@sentry/node': 10.57.0 + axios: 1.15.0 + transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' + - debug + - supports-color + chai@6.2.2: {} chalk@4.1.2: @@ -6112,6 +6344,8 @@ snapshots: chalk@5.6.2: {} + cjs-module-lexer@2.2.0: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -6784,6 +7018,13 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@3.0.2: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -7044,6 +7285,8 @@ snapshots: minimist@1.2.8: {} + module-details-from-path@1.0.4: {} + ms@2.1.3: {} msw@2.14.5(@types/node@25.6.2)(typescript@5.8.3): @@ -7464,6 +7707,13 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + reselect@5.1.1: {} resolve-from@4.0.0: {} @@ -7876,7 +8126,7 @@ snapshots: jiti: 2.7.0 lightningcss: 1.32.0 - vitest@4.1.6(@types/node@25.6.2)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.5(@types/node@25.6.2)(typescript@5.8.3))(vite@7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)): + vitest@4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.6.2)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.5(@types/node@25.6.2)(typescript@5.8.3))(vite@7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)): dependencies: '@vitest/expect': 4.1.6 '@vitest/mocker': 4.1.6(msw@2.14.5(@types/node@25.6.2)(typescript@5.8.3))(vite@7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)) @@ -7899,6 +8149,7 @@ snapshots: vite: 7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/node': 25.6.2 jsdom: 29.1.1(@noble/hashes@1.8.0) transitivePeerDependencies: diff --git a/src/Component/ui/DropDown.tsx b/src/Component/ui/DropDown.tsx index ff9fe84..86459b7 100644 --- a/src/Component/ui/DropDown.tsx +++ b/src/Component/ui/DropDown.tsx @@ -118,11 +118,7 @@ const DropDown: React.FC = ({ )} - {error && ( -

- {error} -

- )} + {error &&

{error}

} ); }; diff --git a/src/Component/ui/Input.tsx b/src/Component/ui/Input.tsx index edf845e..c8da03a 100644 --- a/src/Component/ui/Input.tsx +++ b/src/Component/ui/Input.tsx @@ -6,7 +6,7 @@ type InputType = "text" | "email" | "password" | "number" | "url" | "tel" | "tim type InputProps = { label?: string; name: string; - + placeholder?: string; value?: string | number; type?: InputType; diff --git a/src/features/AddMember/v1/Component/Community_Involment.tsx b/src/features/AddMember/v1/Component/Community_Involment.tsx index b6ad07e..0adf2ca 100644 --- a/src/features/AddMember/v1/Component/Community_Involment.tsx +++ b/src/features/AddMember/v1/Component/Community_Involment.tsx @@ -7,9 +7,9 @@ import { useFormContext } from "react-hook-form"; import type { MemberFormValues } from "../Validator/AddMember.Validator"; const Community_Involvement = () => { - const { watch, setValue , formState} = useFormContext(); + const { watch, setValue, formState } = useFormContext(); - const {errors} = formState + const { errors } = formState; const [internalNotes, setInternalNotes] = useState(watch("internalNotes") ?? ""); const areaOfInterest = watch("areaOfInterest") ?? []; @@ -48,7 +48,6 @@ const Community_Involvement = () => { toggleInterest(interest, clicked)} /> diff --git a/src/features/AddMember/v1/Component/MemberShip_Status.tsx b/src/features/AddMember/v1/Component/MemberShip_Status.tsx index 54e6d1d..6d720a9 100644 --- a/src/features/AddMember/v1/Component/MemberShip_Status.tsx +++ b/src/features/AddMember/v1/Component/MemberShip_Status.tsx @@ -14,8 +14,8 @@ const MemberShip_Status = () => { "On Boarding": "bg-blue-400", }; - const { watch, setValue , formState} = useFormContext(); - const {errors} = formState + const { watch, setValue, formState } = useFormContext(); + const { errors } = formState; const membershipStatus = (watch("membershipStatus") ?? "On Boarding") as MembershipStatus; return ( @@ -47,7 +47,6 @@ const MemberShip_Status = () => { type="radio" id="Inactive" name="membershipStatus" - value="Inactive" checked={membershipStatus === "Inactive"} onChange={() => setValue("membershipStatus", "Inactive", { shouldDirty: true })} diff --git a/src/features/AddMember/v1/Component/SkillChip.tsx b/src/features/AddMember/v1/Component/SkillChip.tsx index 108743c..72ac8cf 100644 --- a/src/features/AddMember/v1/Component/SkillChip.tsx +++ b/src/features/AddMember/v1/Component/SkillChip.tsx @@ -3,9 +3,7 @@ type SkillChipProps = { }; const SkillChip = (props: SkillChipProps) => { - return ( -
{props.skill}
- ); + return
{props.skill}
; }; export default SkillChip; diff --git a/src/features/AddMember/v1/Sections/PersonalInfoCard.tsx b/src/features/AddMember/v1/Sections/PersonalInfoCard.tsx index 2ec0ecb..73acef7 100644 --- a/src/features/AddMember/v1/Sections/PersonalInfoCard.tsx +++ b/src/features/AddMember/v1/Sections/PersonalInfoCard.tsx @@ -7,9 +7,9 @@ import { useFormContext } from "react-hook-form"; import type { MemberFormValues } from "../Validator/AddMember.Validator"; const PersonalInfoCard = () => { - const { setValue, watch , formState } = useFormContext(); + const { setValue, watch, formState } = useFormContext(); - const {errors} = formState; + const { errors } = formState; const firstName = watch("firstName") ?? ""; const lastName = watch("lastName") ?? ""; @@ -59,9 +59,6 @@ const PersonalInfoCard = () => {
- - - { value={firstName} onChange={(_, value) => setValue("firstName", value, { shouldDirty: true })} /> - + { - const { watch, setValue , formState} = useFormContext(); + const { watch, setValue, formState } = useFormContext(); - const {errors} = formState + const { errors } = formState; const [skillInput, setSkillInput] = React.useState(""); const location = watch("location") ?? ""; diff --git a/src/features/AddMember/v1/Validator/AddMember.Validator.ts b/src/features/AddMember/v1/Validator/AddMember.Validator.ts index 15ce823..20c94f3 100644 --- a/src/features/AddMember/v1/Validator/AddMember.Validator.ts +++ b/src/features/AddMember/v1/Validator/AddMember.Validator.ts @@ -33,7 +33,6 @@ export const MemberValidationSchema = z.object({ skills: z.array(z.string()).optional(), areaOfInterest: z.array(z.string()).min(1, "Area of interest is required"), internalNotes: z.string().optional(), - }); export type MemberFormValues = z.input; diff --git a/src/features/Billing/v1/components/layout/QuickRecharge.tsx b/src/features/Billing/v1/components/layout/QuickRecharge.tsx index c460434..1cc8bbf 100644 --- a/src/features/Billing/v1/components/layout/QuickRecharge.tsx +++ b/src/features/Billing/v1/components/layout/QuickRecharge.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; import { CreditCard, Loader2, Zap } from "lucide-react"; import Input from "@/Component/ui/Input"; import { formatRupees, buildAddFundsPreview, formatCredits } from "../../utils/credits"; @@ -6,10 +6,61 @@ import { useAddFunds } from "../../hooks/useWallet"; import { useToast } from "@/features/Tasks/v1/components/common/ToastNotification"; import { MIN_ADD_RUPEES } from "../../constants/billing.constants"; +import { useCreatePaymentIntent } from "../../hook/usePayment"; +import { handlePayType } from "../../type/handlePay.type"; +import { getCashfree } from "@/lib/cashfree"; + export default function QuickRecharge() { const addFunds = useAddFunds(); + const { addToast } = useToast(); const [amountStr, setAmountStr] = useState("500"); + const createPaymentIntent = useCreatePaymentIntent(); + + 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."); + } + }, []); + + 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."); + } + }, []); const handleRecharge = async () => { const amt = Number(amountStr) || 0; @@ -19,6 +70,7 @@ export default function QuickRecharge() { } try { + await createPayment(); await addFunds.mutateAsync({ amountRupees: amt, paymentMethod: "upi", diff --git a/src/features/Billing/v1/hook/usePayment.ts b/src/features/Billing/v1/hook/usePayment.ts new file mode 100644 index 0000000..092b92d --- /dev/null +++ b/src/features/Billing/v1/hook/usePayment.ts @@ -0,0 +1,28 @@ +import api from "@/utils/axios.utils"; +import { useMutation } from "@tanstack/react-query"; + +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; + }, + + onSuccess: (response) => { + console.log("Payment intent created successfully:", response.data); + }, + + onError: (error) => { + console.error("Failed to create payment intent:", error); + }, + }); +}; diff --git a/src/features/Billing/v1/type/handlePay.type.ts b/src/features/Billing/v1/type/handlePay.type.ts new file mode 100644 index 0000000..140d757 --- /dev/null +++ b/src/features/Billing/v1/type/handlePay.type.ts @@ -0,0 +1,4 @@ +export type handlePayType = { + orderId: string; + session_id: string; +}; diff --git a/src/lib/cashfree.ts b/src/lib/cashfree.ts new file mode 100644 index 0000000..7c883a3 --- /dev/null +++ b/src/lib/cashfree.ts @@ -0,0 +1,16 @@ +import { load, type Cashfree } from "@cashfreepayments/cashfree-js"; + +let cashfreeInstance: Cashfree | null = null; + +export async function getCashfree(): Promise { + if (cashfreeInstance) { + return cashfreeInstance; + } + + cashfreeInstance = await load({ + mode: import.meta.env.VITE_CASHFREE_ENV === "production" ? "production" : "sandbox", + + }); + + return cashfreeInstance; +} diff --git a/src/routes/OrgRoute.tsx b/src/routes/OrgRoute.tsx index c8e80b4..c26de9a 100644 --- a/src/routes/OrgRoute.tsx +++ b/src/routes/OrgRoute.tsx @@ -13,8 +13,6 @@ import EditTaskPage from "@/features/Tasks/v1/pages/EditTaskPage"; import TaskDetailPage from "@/features/Tasks/v1/pages/TaskDetailPage"; import TaskManagementPage from "@/features/Tasks/v1/pages/TaskManagementPage"; -import ProtectedRoute from "./ProtectedRoute"; - // Lazy-loaded Webhook pages const WebhookListPage = lazy(() => import("@/features/Webhooks/v1/pages/WebhookListPage")); const CreateWebhookPage = lazy(() => import("@/features/Webhooks/v1/pages/CreateWebhookPage")); @@ -53,22 +51,10 @@ const OrgRoute = () => { } /> } /> } /> - - {/* Webhooks */} - - - } /> - } /> - } /> - } /> - } /> - - - } - /> + } /> + } /> + } /> + } /> {/* Community Wallet */} } /> diff --git a/src/types/cashfree.d.ts b/src/types/cashfree.d.ts new file mode 100644 index 0000000..b25f584 --- /dev/null +++ b/src/types/cashfree.d.ts @@ -0,0 +1,30 @@ +declare module "@cashfreepayments/cashfree-js" { + export interface CashfreeCheckoutOptions { + paymentSessionId: string; + redirectTarget?: "_modal" | "_self" | "_blank"; + } + + export interface CashfreeCheckoutResult { + error?: { + code?: string; + message?: string; + }; + redirect?: boolean; + paymentDetails?: { + paymentMessage?: string; + paymentStatus?: string; + paymentAmount?: number; + paymentMethod?: string; + }; + } + + export interface Cashfree { + checkout(options: CashfreeCheckoutOptions): Promise; + } + + export interface LoadOptions { + mode: "sandbox" | "production"; + } + + export function load(options: LoadOptions): Promise; +}