diff --git a/SALESFORCE_BACKEND_CALLBACK_UPDATE.md b/SALESFORCE_BACKEND_CALLBACK_UPDATE.md
new file mode 100644
index 0000000..19b6971
--- /dev/null
+++ b/SALESFORCE_BACKEND_CALLBACK_UPDATE.md
@@ -0,0 +1,314 @@
+# ๐ Salesforce OAuth - Backend Callback Flow Update
+
+## What Changed?
+
+The Salesforce OAuth implementation has been updated to use a **backend callback flow** instead of a mobile deep-link scheme. This means the backend server handles the OAuth callback and token exchange directly.
+
+---
+
+## Previous Flow vs New Flow
+
+### โ Previous Flow (Deep Link)
+```
+Salesforce โ Mobile App (deep link) โ Send code to backend โ Backend exchanges token
+```
+- **Redirect URI**: `centralizedreportingsystem://oauth/salesforce/callback`
+- **Problem**: Custom scheme requires complex deep linking configuration
+- **Risk**: Authorization code exposed to mobile app
+
+### โ
New Flow (Backend Callback)
+```
+Salesforce โ Backend Server โ Exchange token โ Success page โ Mobile app detects success
+```
+- **Redirect URI**: `https://YOUR_BACKEND_URL/api/v1/users/oauth/callback?user_uuid=USER_ID&service_name=salesforce`
+- **Benefits**:
+ - Backend handles token exchange securely
+ - No need for deep linking configuration
+ - Authorization code never exposed to mobile app
+ - Standard OAuth 2.0 web flow
+
+---
+
+## Code Changes
+
+### SalesforceAuth.tsx
+
+#### 1. Updated Configuration
+```typescript
+// OLD
+const SALESFORCE_CONFIG = {
+ CLIENT_ID: 'YOUR_CLIENT_ID',
+ REDIRECT_URI: 'centralizedreportingsystem://oauth/salesforce/callback',
+ AUTH_BASE_URL: 'https://login.salesforce.com',
+};
+
+// NEW
+const SALESFORCE_CONFIG = {
+ CLIENT_ID: 'YOUR_CLIENT_ID',
+ BACKEND_BASE_URL: 'https://d5285bf63993.ngrok-free.app',
+ CALLBACK_PATH: '/api/v1/users/oauth/callback',
+ AUTH_BASE_URL: 'https://login.salesforce.com',
+};
+```
+
+#### 2. Updated OAuth URL Builder
+```typescript
+// OLD
+const buildSalesforceAuthUrl = (): string => {
+ const redirectUri = 'centralizedreportingsystem://oauth/salesforce/callback';
+ // ...
+};
+
+// NEW
+const buildSalesforceAuthUrl = (userUuid: string): string => {
+ // Build redirect URI with query parameters for backend
+ const redirectUri = `${BACKEND_BASE_URL}${CALLBACK_PATH}?user_uuid=${userUuid}&service_name=salesforce`;
+ // ...
+};
+```
+
+#### 3. New Detection Functions
+```typescript
+// Check if URL is the backend callback
+const isBackendCallbackUri = (url: string): boolean => {
+ return url.includes('/api/v1/users/oauth/callback');
+};
+
+// Check for success
+const isCallbackSuccess = (url: string): boolean => {
+ const status = getQueryParamFromUrl(url, 'status');
+ return status === 'success';
+};
+
+// Check for error
+const isCallbackError = (url: string): boolean => {
+ const status = getQueryParamFromUrl(url, 'status');
+ return status === 'error' || status === 'failure';
+};
+```
+
+#### 4. Simplified Navigation Handler
+```typescript
+// OLD - Mobile app handled token exchange
+handleNavigationStateChange = (navState) => {
+ if (isRedirectUri(url) && authCode) {
+ handleAuthorizationCode(authCode); // Send code to backend
+ }
+};
+
+// NEW - Backend handles token exchange
+handleNavigationStateChange = (navState) => {
+ if (isBackendCallbackUri(url)) {
+ if (isCallbackSuccess(url)) {
+ handleBackendSuccess(); // Just close modal
+ }
+ if (isCallbackError(url)) {
+ handleBackendError(url); // Show error
+ }
+ }
+};
+```
+
+---
+
+## Backend Implementation Required
+
+### Endpoint: `GET /api/v1/users/oauth/callback`
+
+Your backend **must** implement this endpoint to handle the OAuth callback from Salesforce.
+
+#### Query Parameters (Received from Salesforce):
+```typescript
+{
+ user_uuid: string; // User ID (passed in redirect_uri)
+ service_name: string; // 'salesforce' (passed in redirect_uri)
+ code: string; // Authorization code from Salesforce
+}
+```
+
+#### Backend Responsibilities:
+
+1. **Receive the callback** from Salesforce
+2. **Extract** query parameters: `user_uuid`, `service_name`, `code`
+3. **Exchange** authorization code for tokens with Salesforce:
+ ```http
+ POST https://login.salesforce.com/services/oauth2/token
+ grant_type=authorization_code
+ code={CODE}
+ client_id={CLIENT_ID}
+ client_secret={CLIENT_SECRET}
+ redirect_uri={CALLBACK_URL_WITH_PARAMS}
+ ```
+4. **Store** tokens (encrypted) in database
+5. **Return** success/error HTML page:
+ - Success: Redirect to `?status=success`
+ - Error: Redirect to `?status=error&message=ERROR_MESSAGE`
+
+#### Example Response (Success):
+```html
+
+
+
Success
+
+ โ Authentication Successful!
+ Your Salesforce account has been connected.
+
+
+
+```
+
+See [SALESFORCE_BACKEND_CALLBACK_FLOW.md](src/modules/integrations/screens/SALESFORCE_BACKEND_CALLBACK_FLOW.md) for complete backend implementation guide.
+
+---
+
+## Configuration Steps
+
+### 1. Update Salesforce Connected App
+
+In your Salesforce Connected App settings:
+
+**Old Callback URL**:
+```
+centralizedreportingsystem://oauth/salesforce/callback
+```
+
+**New Callback URL**:
+```
+https://YOUR_BACKEND_URL/api/v1/users/oauth/callback
+```
+
+> Note: Don't include query parameters in the Salesforce Connected App config. The app adds them dynamically.
+
+### 2. Update Mobile App (Already Done โ
)
+
+The `SalesforceAuth.tsx` component has been updated with:
+- โ
New backend callback URL configuration
+- โ
Detection logic for backend callback
+- โ
Success/error handling
+- โ
User UUID passed to backend
+
+### 3. Configure Backend
+
+Update your environment variables:
+
+```bash
+# .env
+SALESFORCE_CLIENT_ID=3MVG9GBhY6wQjl2sueQtv2NXMm3EuWtEvOQoeKRAzYcgs2...
+SALESFORCE_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+BACKEND_URL=https://d5285bf63993.ngrok-free.app
+ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
+```
+
+### 4. Implement Backend Endpoint
+
+Create the `/api/v1/users/oauth/callback` endpoint following the guide in [SALESFORCE_BACKEND_CALLBACK_FLOW.md](src/modules/integrations/screens/SALESFORCE_BACKEND_CALLBACK_FLOW.md).
+
+---
+
+## Testing the Updated Flow
+
+### Test Checklist:
+
+1. **Start Backend Server**
+ - Ensure backend is running
+ - Verify callback endpoint is accessible
+ - For local testing, use ngrok: `ngrok http 4000`
+
+2. **Update Configuration**
+ - Update `BACKEND_BASE_URL` in `SalesforceAuth.tsx`
+ - Update Salesforce Connected App callback URL
+ - Restart mobile app
+
+3. **Test Authentication**
+ - Open app โ CRM & Sales โ Salesforce
+ - Complete Salesforce login
+ - **Expected**: Redirect to backend URL
+ - **Backend logs should show**: "Received callback", "Token exchange successful", "Tokens stored"
+ - **Mobile app should show**: Success message, then close modal
+
+4. **Verify Token Storage**
+ - Check database for stored tokens
+ - Verify tokens are encrypted
+ - Verify associated with correct user_id
+
+5. **Test Re-authentication**
+ - Tap "Re-auth" button
+ - Complete flow again
+ - Verify tokens are updated in database
+
+---
+
+## Debugging
+
+### Check Logs
+
+#### Mobile App (React Native)
+```
+[SalesforceAuth] Built OAuth URL: https://login.salesforce.com/...
+[SalesforceAuth] Redirect URI: https://YOUR_BACKEND/callback?user_uuid=...
+[SalesforceAuth] Backend callback detected
+[SalesforceAuth] Backend callback indicates success
+```
+
+#### Backend Server
+```
+[OAuth Callback] Received: { user_uuid: '...', service_name: 'salesforce', hasCode: true }
+[OAuth Callback] Token exchange successful
+[OAuth Callback] Tokens stored successfully
+```
+
+### Common Issues
+
+| Issue | Solution |
+|-------|----------|
+| Backend not reachable | Use ngrok for local testing: `ngrok http 4000` |
+| Callback URL mismatch | Update Salesforce Connected App with exact backend URL |
+| Token exchange fails | Verify CLIENT_SECRET is correct in backend |
+| Mobile app doesn't detect success | Ensure backend returns HTML with `?status=success` |
+
+---
+
+## Benefits of Backend Callback Flow
+
+โ
**More Secure**: Authorization code never exposed to mobile app
+โ
**Simpler Configuration**: No deep linking setup needed
+โ
**Standard OAuth**: Follows standard web OAuth 2.0 flow
+โ
**Backend Control**: Full control over token exchange and storage
+โ
**Better Error Handling**: Backend can provide detailed error messages
+โ
**Token Refresh**: Backend can implement automatic token refresh
+
+---
+
+## Migration Checklist
+
+- [x] Update `SalesforceAuth.tsx` configuration
+- [x] Update OAuth URL builder to include user_uuid
+- [x] Add backend callback detection logic
+- [x] Simplify navigation handler
+- [ ] **Implement backend `/oauth/callback` endpoint**
+- [ ] **Update Salesforce Connected App callback URL**
+- [ ] **Configure backend environment variables**
+- [ ] **Test end-to-end flow**
+- [ ] **Verify token storage in database**
+
+---
+
+## Next Steps
+
+1. **Implement Backend Endpoint**: Follow the guide in [SALESFORCE_BACKEND_CALLBACK_FLOW.md](src/modules/integrations/screens/SALESFORCE_BACKEND_CALLBACK_FLOW.md)
+
+2. **Update Salesforce Config**: Change callback URL in Connected App settings
+
+3. **Test Thoroughly**: Test authentication flow with both success and error scenarios
+
+4. **Deploy**: Deploy backend changes and update mobile app configuration
+
+---
+
+**Updated**: October 2025
+**Status**: โ
Mobile App Updated - Backend Implementation Required
+**Version**: 2.0.0
+
diff --git a/SALESFORCE_INTEGRATION_SUMMARY.md b/SALESFORCE_INTEGRATION_SUMMARY.md
new file mode 100644
index 0000000..93ef757
--- /dev/null
+++ b/SALESFORCE_INTEGRATION_SUMMARY.md
@@ -0,0 +1,380 @@
+# ๐ Salesforce Authentication Integration - Implementation Summary
+
+## Overview
+Successfully implemented Salesforce OAuth authentication for the Centralized Reporting System, enabling users to authenticate with Salesforce through the CRM & Sales Integration category.
+
+---
+
+## โ
What Was Implemented
+
+### 1. **SalesforceAuth Component** (`src/modules/integrations/screens/SalesforceAuth.tsx`)
+ - Full OAuth 2.0 authentication flow using WebView
+ - Mobile-optimized with `display=touch` parameter
+ - Authorization code capture and exchange
+ - Comprehensive error handling
+ - Loading states with processing modal
+ - Deep linking support for OAuth callback
+ - Similar architecture to ZohoAuth for consistency
+
+### 2. **Updated IntegrationCategoryScreen** (`src/modules/integrations/screens/IntegrationCategoryScreen.tsx`)
+ - Added Salesforce authentication support
+ - Added `checkSalesforceToken()` function to verify existing tokens
+ - Updated `handleReAuthenticate()` to support both Zoho and Salesforce
+ - Added Salesforce Auth modal integration
+ - Enhanced authentication logic to distinguish between services
+ - Added re-authentication button for Salesforce
+
+### 3. **Deep Linking Configuration**
+ - **iOS**: Updated `Info.plist` with `CFBundleURLTypes` for custom URL scheme
+ - **Android**: Already configured in `AndroidManifest.xml` (existing deep linking support)
+ - URL Scheme: `centralizedreportingsystem://oauth/salesforce/callback`
+
+### 4. **Comprehensive Setup Guide** (`src/modules/integrations/screens/SALESFORCE_SETUP.md`)
+ - Step-by-step Salesforce Connected App creation
+ - OAuth configuration instructions
+ - Mobile app configuration guide
+ - Backend implementation guidance
+ - Troubleshooting section
+ - Security best practices
+ - Testing procedures
+
+---
+
+## ๐ง Configuration Required
+
+### Step 1: Create Salesforce Connected App
+
+1. **Access Salesforce Setup**
+ - Log in to Salesforce (Production or Sandbox)
+ - Click gear icon (โ๏ธ) โ Setup
+
+2. **Create Connected App**
+ - Navigate to App Manager
+ - Click "New Connected App"
+ - Fill in basic information:
+ - **Name**: `Centralized Reporting System`
+ - **Contact Email**: Your email
+
+3. **Enable OAuth Settings**
+ - Check "Enable OAuth Settings"
+ - **Callback URL**: `centralizedreportingsystem://oauth/salesforce/callback`
+ - **Select OAuth Scopes**:
+ - Access the identity URL service (id, profile, email, address, phone)
+ - Access unique user identifiers (openid)
+ - Perform requests at any time (refresh_token, offline_access)
+ - Manage user data via APIs (api)
+
+4. **Save and Retrieve Credentials**
+ - Wait 2-10 minutes for processing
+ - Navigate back to App Manager โ View your app
+ - Copy the **Consumer Key** (Client ID)
+ - Reveal and copy the **Consumer Secret** (Client Secret)
+
+### Step 2: Update Application Configuration
+
+Open `src/modules/integrations/screens/SalesforceAuth.tsx` and update:
+
+```typescript
+const SALESFORCE_CONFIG = {
+ // โ ๏ธ REPLACE with your Consumer Key from Salesforce
+ CLIENT_ID: 'YOUR_CONSUMER_KEY_HERE',
+
+ // This must match exactly what you configured in Salesforce
+ REDIRECT_URI: 'centralizedreportingsystem://oauth/salesforce/callback',
+
+ // Production: https://login.salesforce.com
+ // Sandbox: https://test.salesforce.com
+ AUTH_BASE_URL: 'https://login.salesforce.com',
+
+ RESPONSE_TYPE: 'code',
+ DISPLAY: 'touch',
+};
+```
+
+### Step 3: Backend Token Exchange
+
+Your backend needs to implement the token exchange endpoint:
+
+**Endpoint**: `POST /api/v1/integrations/manage-token`
+
+**Request Body**:
+```json
+{
+ "authorization_code": "aPrxXXXXXXXXX",
+ "id": "user-uuid",
+ "service_name": "salesforce",
+ "access_token": "user-session-token"
+}
+```
+
+**Backend Should**:
+1. Receive the authorization code
+2. Exchange it with Salesforce for an access token:
+ ```http
+ POST https://login.salesforce.com/services/oauth2/token
+ Content-Type: application/x-www-form-urlencoded
+
+ grant_type=authorization_code
+ &code={AUTH_CODE}
+ &client_id={CLIENT_ID}
+ &client_secret={CLIENT_SECRET}
+ &redirect_uri={REDIRECT_URI}
+ ```
+3. Store tokens securely (encrypted)
+4. Return success/failure response
+
+**Salesforce Response**:
+```json
+{
+ "access_token": "00D...ABC",
+ "refresh_token": "5Aep...XYZ",
+ "instance_url": "https://yourinstance.salesforce.com",
+ "id": "https://login.salesforce.com/id/00D.../005...",
+ "token_type": "Bearer",
+ "issued_at": "1699999999999"
+}
+```
+
+### Step 4: Optional - Data Sync Endpoint
+
+If you want to sync Salesforce data, implement:
+
+**Endpoint**: `POST /api/v1/integrations/salesforce/sync/schedule`
+
+This is called automatically after successful authentication to initiate data synchronization.
+
+---
+
+## ๐ How to Use
+
+### For End Users:
+
+1. **Navigate to Integration**
+ - Open app โ Integrations tab
+ - Tap "CRM & Sales Integration"
+
+2. **Select Salesforce**
+ - Tap on "Salesforce" service
+ - If not authenticated, Salesforce login screen will appear
+
+3. **Authenticate**
+ - Enter Salesforce credentials
+ - Complete 2FA if enabled
+ - Approve OAuth permissions
+
+4. **Success**
+ - Authorization code is captured
+ - Backend exchanges code for tokens
+ - Processing modal shows progress
+ - Modal closes automatically on success
+
+5. **Re-authenticate (Optional)**
+ - Tap the "Re-auth" button next to Salesforce
+ - Useful for changing organizations or refreshing permissions
+
+---
+
+## ๐๏ธ File Structure
+
+```
+src/modules/integrations/screens/
+โโโ SalesforceAuth.tsx # New: Salesforce OAuth component
+โโโ ZohoAuth.tsx # Existing: Zoho OAuth component
+โโโ IntegrationCategoryScreen.tsx # Updated: Added Salesforce support
+โโโ SALESFORCE_SETUP.md # New: Setup documentation
+โโโ ...
+
+ios/CentralizedReportingSystem/
+โโโ Info.plist # Updated: Added CFBundleURLTypes
+
+android/app/src/main/
+โโโ AndroidManifest.xml # Already configured (no changes needed)
+```
+
+---
+
+## ๐ Security Considerations
+
+1. **Never Commit Credentials**
+ - Store Client ID and Secret in environment variables
+ - Use `.env` files (don't commit to Git)
+ - Example:
+ ```bash
+ # .env
+ SALESFORCE_CLIENT_ID=your_consumer_key
+ SALESFORCE_CLIENT_SECRET=your_consumer_secret
+ ```
+
+2. **Backend Token Storage**
+ - Encrypt tokens before storing
+ - Use secure database encryption
+ - Implement token rotation
+
+3. **HTTPS Only**
+ - Always use `https://` for Salesforce APIs
+ - Never downgrade to HTTP in production
+
+4. **Minimal Scopes**
+ - Request only necessary OAuth scopes
+ - Avoid `full` access unless required
+
+5. **State Parameter** (Future Enhancement)
+ - Consider adding CSRF protection with state parameter
+ - Validate state on callback
+
+---
+
+## ๐งช Testing Checklist
+
+- [ ] Create Salesforce Connected App
+- [ ] Update CLIENT_ID in SalesforceAuth.tsx
+- [ ] Backend token exchange endpoint is ready
+- [ ] Build and run the app (both iOS and Android)
+- [ ] Navigate to CRM & Sales Integration
+- [ ] Tap Salesforce service
+- [ ] Complete OAuth flow
+- [ ] Verify authorization code is captured
+- [ ] Check backend logs for successful token exchange
+- [ ] Test re-authentication flow
+- [ ] Test with Production Salesforce org
+- [ ] Test with Sandbox Salesforce org (if applicable)
+- [ ] Verify error handling (wrong credentials, network failure)
+- [ ] Test deep linking callback on both platforms
+
+---
+
+## ๐ Common Issues & Solutions
+
+### Issue 1: "redirect_uri_mismatch"
+**Solution**: Ensure the redirect URI in code exactly matches Salesforce Connected App configuration.
+
+### Issue 2: "invalid_client_id"
+**Solution**: Wait 2-10 minutes after creating Connected App, or verify the Client ID is correct.
+
+### Issue 3: WebView Doesn't Load
+**Solution**: Check internet connection, verify AUTH_BASE_URL is correct.
+
+### Issue 4: App Doesn't Capture Code
+**Solution**: Verify deep linking is configured correctly in Info.plist and AndroidManifest.xml.
+
+### Issue 5: Backend Token Exchange Fails
+**Solution**: Verify backend endpoint, check Consumer Secret, ensure authorization code hasn't expired (15 min limit).
+
+---
+
+## ๐ Architecture Flow
+
+```
+User Taps Salesforce
+ โ
+Check for Existing Token (checkSalesforceToken)
+ โ
+ [No Token?]
+ โ
+Open SalesforceAuth Modal
+ โ
+Display Salesforce Login (WebView)
+ โ
+User Authenticates
+ โ
+Salesforce Redirects to: centralizedreportingsystem://oauth/salesforce/callback?code=...
+ โ
+App Captures Authorization Code
+ โ
+Send Code to Backend (manageToken API)
+ โ
+Backend Exchanges Code for Tokens (Salesforce API)
+ โ
+Backend Stores Tokens (Encrypted)
+ โ
+[Optional] Schedule Data Sync
+ โ
+Success! Close Modal
+ โ
+User is Authenticated
+```
+
+---
+
+## ๐ Next Steps
+
+### Immediate (Required):
+1. โ
Create Salesforce Connected App
+2. โ
Update `CLIENT_ID` in `SalesforceAuth.tsx`
+3. โ
Implement backend token exchange endpoint
+4. โ
Test the integration end-to-end
+
+### Future Enhancements:
+- [ ] Add token refresh logic
+- [ ] Implement data synchronization from Salesforce
+- [ ] Add Salesforce dashboard screens
+- [ ] Support multiple Salesforce orgs per user
+- [ ] Add offline token management
+- [ ] Implement webhook support for real-time updates
+- [ ] Add analytics for Salesforce data
+- [ ] Create Salesforce-specific widgets
+
+---
+
+## ๐ Resources
+
+### Documentation Files:
+- **Setup Guide**: `src/modules/integrations/screens/SALESFORCE_SETUP.md`
+- **Component**: `src/modules/integrations/screens/SalesforceAuth.tsx`
+- **Integration Screen**: `src/modules/integrations/screens/IntegrationCategoryScreen.tsx`
+
+### External Resources:
+- [Salesforce Connected Apps](https://help.salesforce.com/s/articleView?id=sf.connected_app_overview.htm)
+- [Salesforce OAuth 2.0](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_flow.htm)
+- [React Native WebView](https://github.com/react-native-webview/react-native-webview)
+
+---
+
+## ๐ฏ Key Features
+
+โ
Full OAuth 2.0 authentication flow
+โ
Mobile-optimized login experience
+โ
Automatic authorization code capture
+โ
Backend token exchange integration
+โ
Re-authentication support
+โ
Comprehensive error handling
+โ
Loading and processing states
+โ
Deep linking configuration
+โ
Production and Sandbox support
+โ
Consistent with existing Zoho integration
+
+---
+
+## ๐ค Support
+
+If you encounter issues:
+1. Check the `SALESFORCE_SETUP.md` guide
+2. Review console logs for error messages
+3. Verify Salesforce Connected App configuration
+4. Check backend logs for token exchange errors
+5. Test with both Production and Sandbox environments
+
+---
+
+**Implementation Date**: October 2025
+**Status**: โ
Complete - Pending Configuration
+**Version**: 1.0.0
+
+---
+
+## ๐ Notes for Developers
+
+- The Salesforce authentication follows the same pattern as Zoho authentication for consistency
+- Authorization codes expire in 15 minutes - ensure timely backend exchange
+- The `manageToken` API endpoint is reused for both Zoho and Salesforce (differentiated by `service_name` parameter)
+- Deep linking is already configured for the app; Salesforce uses the same scheme
+- The component includes comprehensive logging for debugging
+- Error handling includes automatic retry functionality
+- The processing modal provides user feedback during async operations
+
+---
+
+**Ready to Deploy!** ๐
+
+Once you've completed the configuration steps above, the Salesforce integration will be fully functional. Users will be able to authenticate with their Salesforce accounts directly from the CRM & Sales Integration category.
+
diff --git a/ios/CentralizedReportingSystem/Info.plist b/ios/CentralizedReportingSystem/Info.plist
index 4cc9758..fec0c8e 100644
--- a/ios/CentralizedReportingSystem/Info.plist
+++ b/ios/CentralizedReportingSystem/Info.plist
@@ -56,5 +56,16 @@
Roboto-SemiBold.ttf
Roboto-Thin.ttf
+ CFBundleURLTypes
+
+
+ CFBundleURLSchemes
+
+ centralizedreportingsystem
+
+ CFBundleURLName
+ com.centralizedreportingsystem
+
+
diff --git a/src/modules/crm/navigation/CrmNavigator.tsx b/src/modules/crm/navigation/CrmNavigator.tsx
index 64b888a..34ed106 100644
--- a/src/modules/crm/navigation/CrmNavigator.tsx
+++ b/src/modules/crm/navigation/CrmNavigator.tsx
@@ -1,16 +1,30 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
+import { useSelector } from 'react-redux';
+import type { RootState } from '@/store/store';
import CrmDashboardScreen from '@/modules/crm/zoho/screens/CrmDashboardScreen';
import ZohoCrmDataScreen from '@/modules/crm/zoho/screens/ZohoCrmDataScreen';
+import SalesforceCrmDashboardScreen from '@/modules/crm/salesforce/screens/SalesforceCrmDashboardScreen';
+import SalesforceDataScreen from '@/modules/crm/salesforce/screens/SalesforceDataScreen';
const Stack = createStackNavigator();
-const CrmNavigator = () => (
-
-
-
-
-);
+const CrmNavigator = () => {
+ // Get the selected service from Redux to determine initial screen
+ const selectedService = useSelector((state: RootState) => state.integrations.selectedService);
+
+ // Determine initial route based on selected CRM service
+ const initialRouteName = selectedService === 'salesforce' ? 'SalesforceDashboard' : 'CrmDashboard';
+
+ return (
+
+
+
+
+
+
+ );
+};
export default CrmNavigator;
diff --git a/src/modules/crm/salesforce/README.md b/src/modules/crm/salesforce/README.md
new file mode 100644
index 0000000..be38cff
--- /dev/null
+++ b/src/modules/crm/salesforce/README.md
@@ -0,0 +1,265 @@
+# Salesforce CRM Module
+
+## Overview
+This module provides a complete Salesforce CRM integration for the Centralized Reporting System, including dashboard visualizations, data management, and comprehensive analytics.
+
+## Structure
+
+```
+salesforce/
+โโโ components/
+โ โโโ SalesforceDataCards.tsx # Card components for each data type
+โโโ screens/
+โ โโโ SalesforceCrmDashboardScreen.tsx # Dashboard with KPIs and charts
+โ โโโ SalesforceDataScreen.tsx # Tabbed data view with all CRM data
+โโโ services/
+โ โโโ salesforceCrmAPI.ts # API service for Salesforce data
+โโโ store/
+โ โโโ salesforceCrmSlice.ts # Redux slice with state management
+โ โโโ selectors.ts # Redux selectors
+โโโ types/
+โ โโโ SalesforceCrmTypes.ts # TypeScript type definitions
+โโโ index.ts # Module exports
+```
+
+## API Endpoints
+
+The module integrates with the following Salesforce CRM endpoints:
+
+- **Events**: `/api/v1/n8n/salesforce/crm/events`
+- **Leads**: `/api/v1/n8n/salesforce/crm/leads`
+- **Tasks**: `/api/v1/n8n/salesforce/crm/tasks`
+- **Accounts**: `/api/v1/n8n/salesforce/crm/accounts`
+- **Opportunities**: `/api/v1/n8n/salesforce/crm/opportunities`
+
+## Features
+
+### Dashboard Screen (`SalesforceCrmDashboardScreen`)
+- **Hero Stats**: Large cards showing Leads and Opportunities totals
+- **Pipeline Overview**: Total pipeline value, average deal size, and open deals
+- **Module Counts**: Compact grid showing Events, Tasks, and Accounts counts
+- **Lead Status Distribution**: Pie chart showing New, Working, and Qualified leads
+- **Task Status**: Stacked bar chart showing Open, Completed, and High Priority tasks
+- **Key Metrics**: Upcoming Events and Won Deals summary
+- **Pull-to-refresh** functionality
+- **Real-time data updates**
+
+### Data Screen (`SalesforceDataScreen`)
+- **Tabbed Interface**: Switch between Events, Leads, Tasks, Accounts, and Opportunities
+- **Data Cards**: Rich card display for each data type with relevant information
+- **Infinite Scrolling**: Load more data as you scroll
+- **Count Badges**: Show total count for each tab
+- **Pull-to-refresh** functionality
+- **Error handling and loading states**
+
+### Data Types
+
+#### Event
+- Subject, Location, Description
+- Start/End DateTime
+- All Day Event indicator
+- Created date
+
+#### Lead
+- Name (First + Last)
+- Company, Email, Phone
+- Status (New, Working, Nurturing, Qualified, Unqualified)
+- Title, Lead Source
+- Created date
+
+#### Task
+- Subject, Description
+- Status (Open, In Progress, Completed, Waiting, Deferred)
+- Priority (High, Normal, Low)
+- Activity Date
+- Created/Modified dates
+
+#### Account
+- Name, Industry, Type
+- Phone, Website
+- Billing Address (City, State, Country)
+- Annual Revenue
+- Created date
+
+#### Opportunity
+- Name, Stage, Type
+- Amount, Probability
+- Close Date, Forecast Category
+- Created/Modified dates
+
+## Redux State Management
+
+### State Structure
+```typescript
+{
+ events: SalesforceEvent[];
+ leads: SalesforceLead[];
+ tasks: SalesforceTask[];
+ accounts: SalesforceAccount[];
+ opportunities: SalesforceOpportunity[];
+ loading: { /* loading states for each type */ };
+ errors: { /* error states for each type */ };
+ pagination: { /* pagination info for each type */ };
+ counts: { /* total counts from API */ };
+ lastUpdated: { /* last updated timestamps */ };
+}
+```
+
+### Actions
+- `fetchAllSalesforceCrmData()` - Fetch all data types in parallel
+- `fetchSalesforceEvents()` - Fetch events with pagination
+- `fetchSalesforceLeads()` - Fetch leads with pagination
+- `fetchSalesforceTasks()` - Fetch tasks with pagination
+- `fetchSalesforceAccounts()` - Fetch accounts with pagination
+- `fetchSalesforceOpportunities()` - Fetch opportunities with pagination
+- `fetchSalesforceCrmCounts()` - Fetch total counts for all types
+- `resetEventsPagination()` - Reset events pagination
+- `resetLeadsPagination()` - Reset leads pagination
+- `resetTasksPagination()` - Reset tasks pagination
+- `resetAccountsPagination()` - Reset accounts pagination
+- `resetOpportunitiesPagination()` - Reset opportunities pagination
+- `clearData()` - Clear all data
+- `clearErrors()` - Clear all errors
+
+### Selectors
+- `selectSalesforceEvents` - Get events array
+- `selectSalesforceLeads` - Get leads array
+- `selectSalesforceTasks` - Get tasks array
+- `selectSalesforceAccounts` - Get accounts array
+- `selectSalesforceOpportunities` - Get opportunities array
+- `selectSalesforceCrmLoading` - Get loading states
+- `selectSalesforceCrmErrors` - Get error states
+- `selectSalesforceCrmCounts` - Get total counts
+- `selectDashboardData` - Get computed dashboard data with summary
+- `selectIsAnyLoading` - Check if any data is loading
+- `selectHasAnyError` - Check if any errors exist
+
+## Navigation
+
+The module is integrated into the CRM navigator:
+
+```typescript
+
+
+```
+
+## Usage
+
+### Navigate to Salesforce Dashboard
+```typescript
+navigation.navigate('SalesforceDashboard');
+```
+
+### Navigate to Salesforce Data Screen
+```typescript
+navigation.navigate('SalesforceData');
+```
+
+### Fetch Salesforce Data
+```typescript
+import { useDispatch } from 'react-redux';
+import { fetchAllSalesforceCrmData } from '@/modules/crm/salesforce';
+
+const dispatch = useDispatch();
+
+// Fetch all data
+await dispatch(fetchAllSalesforceCrmData()).unwrap();
+
+// Fetch specific data type with pagination
+await dispatch(fetchSalesforceLeads({ page: 1, limit: 20 })).unwrap();
+```
+
+### Access Salesforce Data
+```typescript
+import { useSelector } from 'react-redux';
+import { selectSalesforceLeads, selectDashboardData } from '@/modules/crm/salesforce';
+
+// Get leads
+const leads = useSelector(selectSalesforceLeads);
+
+// Get dashboard data with computed summaries
+const dashboardData = useSelector(selectDashboardData);
+```
+
+## API Response Format
+
+All endpoints return responses in the following format:
+
+```json
+{
+ "status": "success",
+ "message": "Salesforce crm {resource} data fetched successfully",
+ "data": {
+ "success": true,
+ "data": [ /* array of records */ ],
+ "count": 56,
+ "metadata": {
+ "totalSize": 56,
+ "done": true,
+ "nextRecordsUrl": null
+ }
+ },
+ "timestamp": "2025-10-09T06:40:31.428Z"
+}
+```
+
+## Styling
+
+The module follows the application's design system:
+- Uses theme colors from `useTheme()` hook
+- Consistent spacing and typography
+- Responsive layouts
+- Material Community Icons
+- Smooth animations and transitions
+
+## Data Persistence
+
+The Salesforce CRM state is persisted using Redux Persist:
+- State is saved to AsyncStorage
+- Survives app restarts
+- Automatically rehydrated on app launch
+
+## Error Handling
+
+- All API calls wrapped in try-catch blocks
+- Error states stored in Redux
+- User-friendly error messages via Toast notifications
+- Retry functionality on error screens
+- Loading states for all async operations
+
+## Performance Optimizations
+
+- **Parallel Data Fetching**: All data types fetched simultaneously
+- **Infinite Scrolling**: Load data in chunks to reduce initial load time
+- **Memoized Selectors**: Prevent unnecessary re-renders
+- **Redux Persist**: Cache data locally to reduce API calls
+- **Pull-to-refresh**: Manual refresh when needed
+- **Optimized Re-renders**: Use React.memo where appropriate
+
+## Future Enhancements
+
+- Advanced filtering options
+- Search functionality
+- Sort options for each data type
+- Export data to CSV/Excel
+- Detailed view screens for each record
+- Create/Edit/Delete operations
+- Real-time sync with Salesforce
+- Offline mode support
+- Analytics and reporting
+- Custom dashboard widgets
+
+## Related Modules
+
+- **Zoho CRM** (`src/modules/crm/zoho/`) - Similar structure for Zoho integration
+- **CRM Navigator** (`src/modules/crm/navigation/CrmNavigator.tsx`) - Navigation configuration
+- **Root Store** (`src/store/store.ts`) - Redux store configuration
+
diff --git a/src/modules/crm/salesforce/components/SalesforceDataCards.tsx b/src/modules/crm/salesforce/components/SalesforceDataCards.tsx
new file mode 100644
index 0000000..4ea4fdd
--- /dev/null
+++ b/src/modules/crm/salesforce/components/SalesforceDataCards.tsx
@@ -0,0 +1,453 @@
+import React from 'react';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
+import { useTheme } from '@/shared/styles/useTheme';
+import type { SalesforceEvent, SalesforceLead, SalesforceTask, SalesforceAccount, SalesforceOpportunity } from '../types/SalesforceCrmTypes';
+
+interface BaseCardProps {
+ onPress: () => void;
+}
+
+interface EventCardProps extends BaseCardProps {
+ event: SalesforceEvent;
+}
+
+interface LeadCardProps extends BaseCardProps {
+ lead: SalesforceLead;
+}
+
+interface TaskCardProps extends BaseCardProps {
+ task: SalesforceTask;
+}
+
+interface AccountCardProps extends BaseCardProps {
+ account: SalesforceAccount;
+}
+
+interface OpportunityCardProps extends BaseCardProps {
+ opportunity: SalesforceOpportunity;
+}
+
+// Helper function to get status-based colors
+const getStatusColor = (status: string, colors: any) => {
+ switch (status.toLowerCase()) {
+ case 'new':
+ case 'open':
+ case 'prospecting':
+ return '#3AA0FF';
+ case 'working':
+ case 'in progress':
+ case 'qualification':
+ case 'needs analysis':
+ return '#F59E0B';
+ case 'qualified':
+ case 'completed':
+ case 'closed won':
+ return '#22C55E';
+ case 'unqualified':
+ case 'closed lost':
+ return '#EF4444';
+ default:
+ return colors.textLight;
+ }
+};
+
+// Helper function to get priority-based colors
+const getPriorityColor = (priority: string) => {
+ switch (priority.toLowerCase()) {
+ case 'high':
+ return '#EF4444';
+ case 'normal':
+ return '#F59E0B';
+ case 'low':
+ return '#10B981';
+ default:
+ return '#6B7280';
+ }
+};
+
+export const EventCard: React.FC = ({ event, onPress }) => {
+ const { colors, fonts, shadows } = useTheme();
+
+ return (
+
+
+
+
+ {event.Subject}
+
+ {event.IsAllDayEvent && (
+
+
+ All Day
+
+
+ )}
+
+ {event.Location && (
+
+ {event.Location}
+
+ )}
+
+
+
+ {event.StartDateTime && (
+
+
+
+ Start: {new Date(event.StartDateTime).toLocaleString()}
+
+
+ )}
+ {event.EndDateTime && (
+
+
+
+ End: {new Date(event.EndDateTime).toLocaleString()}
+
+
+ )}
+ {event.Description && (
+
+
+
+ {event.Description}
+
+
+ )}
+
+
+
+
+ Created: {new Date(event.CreatedDate).toLocaleDateString()}
+
+
+
+ );
+};
+
+export const LeadCard: React.FC = ({ lead, onPress }) => {
+ const { colors, fonts, shadows } = useTheme();
+
+ return (
+
+
+
+
+ {lead.FirstName} {lead.LastName}
+
+
+
+ {lead.Status}
+
+
+
+
+ {lead.Company}
+
+
+
+
+
+
+
+ {lead.Email}
+
+
+ {lead.Phone && (
+
+
+
+ {lead.Phone}
+
+
+ )}
+ {lead.Title && (
+
+
+
+ {lead.Title}
+
+
+ )}
+ {lead.LeadSource && (
+
+
+
+ Source: {lead.LeadSource}
+
+
+ )}
+
+
+
+
+ Created: {lead.CreatedDate ? new Date(lead.CreatedDate).toLocaleDateString() : 'N/A'}
+
+
+
+ );
+};
+
+export const TaskCard: React.FC = ({ task, onPress }) => {
+ const { colors, fonts, shadows } = useTheme();
+
+ return (
+
+
+
+
+ {task.Subject}
+
+
+
+ {task.Status}
+
+
+
+
+
+
+ {task.Description && (
+
+ {task.Description}
+
+ )}
+
+
+
+ Priority: {task.Priority}
+
+
+ {task.ActivityDate && (
+
+
+
+ Due: {new Date(task.ActivityDate).toLocaleDateString()}
+
+
+ )}
+
+
+
+
+ Created: {new Date(task.CreatedDate).toLocaleDateString()}
+
+
+
+ );
+};
+
+export const AccountCard: React.FC = ({ account, onPress }) => {
+ const { colors, fonts, shadows } = useTheme();
+
+ return (
+
+
+
+
+ {account.Name}
+
+ {account.Type && (
+
+
+ {account.Type}
+
+
+ )}
+
+ {account.Industry && (
+
+ {account.Industry}
+
+ )}
+
+
+
+ {account.Phone && (
+
+
+
+ {account.Phone}
+
+
+ )}
+ {account.Website && (
+
+
+
+ {account.Website}
+
+
+ )}
+ {(account.BillingCity || account.BillingState) && (
+
+
+
+ {[account.BillingCity, account.BillingState, account.BillingCountry].filter(Boolean).join(', ')}
+
+
+ )}
+ {account.AnnualRevenue && (
+
+
+
+ Revenue: ${account.AnnualRevenue.toLocaleString()}
+
+
+ )}
+
+
+
+
+ Created: {new Date(account.CreatedDate).toLocaleDateString()}
+
+
+
+ );
+};
+
+export const OpportunityCard: React.FC = ({ opportunity, onPress }) => {
+ const { colors, fonts, shadows } = useTheme();
+
+ return (
+
+
+
+
+ {opportunity.Name}
+
+
+
+ {opportunity.StageName}
+
+
+
+ {opportunity.Type && (
+
+ {opportunity.Type}
+
+ )}
+
+
+
+
+
+
+ ${opportunity.Amount?.toLocaleString()}
+
+
+
+
+
+ Probability: {opportunity.Probability}%
+
+
+
+
+
+ Close date: {new Date(opportunity.CloseDate).toLocaleDateString()}
+
+
+ {opportunity.ForecastCategory && (
+
+
+
+ Forecast: {opportunity.ForecastCategory}
+
+
+ )}
+
+
+
+
+ Created: {new Date(opportunity.CreatedDate).toLocaleDateString()}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ card: {
+ borderRadius: 12,
+ borderWidth: 1,
+ marginBottom: 16,
+ overflow: 'hidden',
+ },
+ cardHeader: {
+ padding: 16,
+ paddingBottom: 12,
+ },
+ cardTitleRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ marginBottom: 4,
+ },
+ cardTitle: {
+ fontSize: 16,
+ flex: 1,
+ marginRight: 8,
+ },
+ cardSubtitle: {
+ fontSize: 14,
+ },
+ statusBadge: {
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 12,
+ },
+ statusText: {
+ fontSize: 12,
+ },
+ cardContent: {
+ paddingHorizontal: 16,
+ paddingBottom: 12,
+ },
+ infoRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 6,
+ },
+ infoText: {
+ marginLeft: 8,
+ fontSize: 14,
+ flex: 1,
+ },
+ descriptionText: {
+ fontSize: 14,
+ marginBottom: 8,
+ lineHeight: 20,
+ },
+ cardFooter: {
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ backgroundColor: '#F8F9FA',
+ },
+ dateText: {
+ fontSize: 12,
+ },
+});
+
diff --git a/src/modules/crm/salesforce/index.ts b/src/modules/crm/salesforce/index.ts
new file mode 100644
index 0000000..535d643
--- /dev/null
+++ b/src/modules/crm/salesforce/index.ts
@@ -0,0 +1,8 @@
+// Salesforce CRM Module Exports
+export { default as SalesforceCrmDashboardScreen } from './screens/SalesforceCrmDashboardScreen';
+export { default as SalesforceDataScreen } from './screens/SalesforceDataScreen';
+export { default as salesforceCrmSlice } from './store/salesforceCrmSlice';
+export { selectDashboardData, selectIsAnyLoading, selectHasAnyError, selectSalesforceCrmCounts } from './store/selectors';
+export * from './types/SalesforceCrmTypes';
+export { salesforceCrmAPI } from './services/salesforceCrmAPI';
+
diff --git a/src/modules/crm/salesforce/screens/SalesforceCrmDashboardScreen.tsx b/src/modules/crm/salesforce/screens/SalesforceCrmDashboardScreen.tsx
new file mode 100644
index 0000000..7122b5e
--- /dev/null
+++ b/src/modules/crm/salesforce/screens/SalesforceCrmDashboardScreen.tsx
@@ -0,0 +1,468 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, RefreshControl, ScrollView, ActivityIndicator } from 'react-native';
+import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
+import { useSelector, useDispatch } from 'react-redux';
+import { useTheme } from '@/shared/styles/useTheme';
+import { useNavigation } from '@react-navigation/native';
+import { fetchAllSalesforceCrmData, fetchSalesforceCrmCounts } from '../store/salesforceCrmSlice';
+import {
+ selectDashboardData,
+ selectIsAnyLoading,
+ selectHasAnyError
+} from '../store/selectors';
+import type { AppDispatch } from '@/store/store';
+import { PieChart, DonutChart, StackedBarChart } from '@/shared/components/charts';
+
+const SalesforceCrmDashboardScreen: React.FC = () => {
+ const { colors, fonts } = useTheme();
+ const navigation = useNavigation();
+ const dispatch = useDispatch();
+
+ // Redux selectors
+ const dashboardData = useSelector(selectDashboardData);
+ const isLoading = useSelector(selectIsAnyLoading);
+ const hasError = useSelector(selectHasAnyError);
+ const [refreshing, setRefreshing] = useState(false);
+
+ // Fetch data on component mount
+ useEffect(() => {
+ fetchData();
+ }, [dispatch]);
+
+ // Fetch all Salesforce CRM data
+ const fetchData = async () => {
+ try {
+ await dispatch(fetchAllSalesforceCrmData()).unwrap();
+ await dispatch(fetchSalesforceCrmCounts()).unwrap();
+ } catch (error) {
+ console.error('Failed to fetch Salesforce CRM data:', error);
+ }
+ };
+
+ // Handle refresh
+ const handleRefresh = async () => {
+ setRefreshing(true);
+ await fetchData();
+ setRefreshing(false);
+ };
+
+ return (
+
+ }
+ >
+
+ Salesforce CRM
+ navigation.navigate('SalesforceData' as never)}
+ >
+
+
+ View Data
+
+
+
+
+ {/* Loading State */}
+ {isLoading && !dashboardData.events.length && (
+
+
+
+ Loading Salesforce CRM data...
+
+
+ )}
+
+ {/* Error State */}
+ {hasError && !dashboardData.events.length && (
+
+
+
+ Failed to load data. Pull to refresh.
+
+
+ )}
+
+ {/* Hero Stats - Large Cards */}
+
+
+
+
+ Leads
+
+
+ {dashboardData.counts?.leads || dashboardData.summary.totalLeads}
+
+
+ {dashboardData.summary.newLeads} New ยท {dashboardData.summary.qualifiedLeads} Qualified
+
+
+
+
+
+
+ Opportunities
+
+
+ {dashboardData.counts?.opportunities || dashboardData.summary.totalOpportunities}
+
+
+ {dashboardData.summary.openOpportunities} Open ยท {dashboardData.summary.wonOpportunities} Won
+
+
+
+
+ {/* Pipeline Value Card */}
+
+
+
+ Pipeline Overview
+
+
+
+ Total Pipeline
+
+ ${(dashboardData.summary.totalPipelineValue / 1000).toFixed(1)}K
+
+
+
+ Avg Deal Size
+
+ ${(dashboardData.summary.averageDealSize / 1000).toFixed(1)}K
+
+
+
+ Open Deals
+
+ {dashboardData.summary.openOpportunities}
+
+
+
+
+
+ {/* Module Counts - Compact Grid */}
+
+
+
+
+ {dashboardData.counts?.events || dashboardData.summary.totalEvents}
+
+ Events
+
+
+
+
+
+ {dashboardData.counts?.tasks || dashboardData.summary.totalTasks}
+
+ Tasks
+
+
+
+
+
+ {dashboardData.counts?.accounts || dashboardData.summary.totalAccounts}
+
+ Accounts
+
+
+
+ {/* Lead Status Distribution */}
+
+ Lead Status Distribution
+
+
+ item.value > 0)}
+ colors={colors}
+ fonts={fonts}
+ size={140}
+ />
+
+ {/* Legend */}
+
+ {[
+ { label: 'New', value: dashboardData.summary.newLeads, color: '#3AA0FF' },
+ { label: 'Working', value: dashboardData.summary.workingLeads, color: '#F59E0B' },
+ { label: 'Qualified', value: dashboardData.summary.qualifiedLeads, color: '#22C55E' },
+ ].filter(item => item.value > 0).map((item) => (
+
+
+
+ {item.label} ({item.value})
+
+
+ ))}
+
+
+
+
+ {/* Task Status */}
+
+ Task Status
+
+
+ item.value > 0)}
+ colors={colors}
+ fonts={fonts}
+ height={120}
+ />
+
+ {/* Legend */}
+
+ {[
+ { label: 'Open', value: dashboardData.summary.openTasks, color: '#3AA0FF' },
+ { label: 'Completed', value: dashboardData.summary.completedTasks, color: '#22C55E' },
+ { label: 'High Priority', value: dashboardData.summary.highPriorityTasks, color: '#EF4444' },
+ ].filter(item => item.value > 0).map((item) => (
+
+
+
+ {item.label} ({item.value})
+
+
+ ))}
+
+
+
+
+ {/* Key Metrics */}
+
+
+
+
+ {dashboardData.summary.upcomingEvents}
+
+ Upcoming Events
+
+
+
+
+
+ {dashboardData.summary.wonOpportunities}
+
+ Won Deals
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16,
+ },
+ scrollContent: {
+ paddingBottom: 20,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ title: {
+ fontSize: 24,
+ },
+ dataButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ borderRadius: 20,
+ },
+ dataButtonText: {
+ marginLeft: 6,
+ fontSize: 14,
+ },
+ loadingCard: {
+ borderRadius: 12,
+ borderWidth: 1,
+ padding: 20,
+ marginBottom: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ loadingText: {
+ marginTop: 12,
+ fontSize: 14,
+ textAlign: 'center',
+ },
+ errorCard: {
+ borderRadius: 12,
+ borderWidth: 1,
+ padding: 12,
+ marginBottom: 16,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ errorText: {
+ marginLeft: 8,
+ fontSize: 14,
+ flex: 1,
+ },
+ heroStatsContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 16,
+ },
+ heroCard: {
+ flex: 1,
+ marginHorizontal: 4,
+ padding: 20,
+ borderRadius: 16,
+ borderWidth: 1,
+ alignItems: 'center',
+ },
+ heroCardHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ heroCardTitle: {
+ fontSize: 16,
+ marginLeft: 8,
+ },
+ heroCardValue: {
+ fontSize: 32,
+ marginBottom: 4,
+ },
+ heroCardSubtitle: {
+ fontSize: 12,
+ textAlign: 'center',
+ },
+ pipelineCard: {
+ padding: 20,
+ borderRadius: 16,
+ borderWidth: 1,
+ marginBottom: 16,
+ },
+ pipelineHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ pipelineTitle: {
+ fontSize: 20,
+ marginLeft: 12,
+ },
+ pipelineStats: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ pipelineItem: {
+ flex: 1,
+ alignItems: 'center',
+ },
+ pipelineLabel: {
+ fontSize: 12,
+ marginBottom: 4,
+ },
+ pipelineValue: {
+ fontSize: 18,
+ },
+ compactGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'space-between',
+ marginBottom: 16,
+ },
+ compactCard: {
+ width: '30%',
+ padding: 12,
+ borderRadius: 12,
+ borderWidth: 1,
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ compactValue: {
+ fontSize: 18,
+ marginVertical: 4,
+ },
+ compactLabel: {
+ fontSize: 10,
+ textAlign: 'center',
+ },
+ card: {
+ borderRadius: 12,
+ borderWidth: 1,
+ padding: 16,
+ marginBottom: 16,
+ },
+ cardTitle: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+ chartContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 10,
+ },
+ pieLegend: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ marginTop: 12,
+ gap: 8,
+ },
+ barLegend: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ marginTop: 8,
+ gap: 6,
+ },
+ legendItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginRight: 12,
+ marginTop: 6,
+ },
+ legendDot: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ marginRight: 6,
+ },
+ legendText: {
+ fontSize: 12,
+ },
+ metricsRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 16,
+ },
+ metricCard: {
+ flex: 1,
+ marginHorizontal: 4,
+ padding: 16,
+ borderRadius: 12,
+ borderWidth: 1,
+ alignItems: 'center',
+ },
+ metricValue: {
+ fontSize: 20,
+ marginVertical: 8,
+ },
+ metricLabel: {
+ fontSize: 12,
+ textAlign: 'center',
+ },
+});
+
+export default SalesforceCrmDashboardScreen;
+
diff --git a/src/modules/crm/salesforce/screens/SalesforceDataScreen.tsx b/src/modules/crm/salesforce/screens/SalesforceDataScreen.tsx
new file mode 100644
index 0000000..7859d1a
--- /dev/null
+++ b/src/modules/crm/salesforce/screens/SalesforceDataScreen.tsx
@@ -0,0 +1,473 @@
+import React, { useState, useEffect, useMemo, useCallback } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ RefreshControl,
+ FlatList,
+ ActivityIndicator,
+} from 'react-native';
+import { useSelector, useDispatch } from 'react-redux';
+import type { AppDispatch } from '@/store/store';
+import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
+import { LoadingSpinner, ErrorState } from '@/shared/components/ui';
+import { useTheme } from '@/shared/styles/useTheme';
+import { showError, showSuccess, showInfo } from '@/shared/utils/Toast';
+import { EventCard, LeadCard, TaskCard, AccountCard, OpportunityCard } from '../components/SalesforceDataCards';
+import {
+ selectSalesforceEvents,
+ selectSalesforceLeads,
+ selectSalesforceTasks,
+ selectSalesforceAccounts,
+ selectSalesforceOpportunities,
+ selectSalesforceCrmLoading,
+ selectSalesforceCrmErrors,
+ selectEventsPagination,
+ selectLeadsPagination,
+ selectTasksPagination,
+ selectAccountsPagination,
+ selectOpportunitiesPagination,
+ selectSalesforceCrmCounts
+} from '../store/selectors';
+import {
+ fetchAllSalesforceCrmData,
+ fetchSalesforceEvents,
+ fetchSalesforceLeads,
+ fetchSalesforceTasks,
+ fetchSalesforceAccounts,
+ fetchSalesforceOpportunities,
+ fetchSalesforceCrmCounts,
+ resetEventsPagination,
+ resetLeadsPagination,
+ resetTasksPagination,
+ resetAccountsPagination,
+ resetOpportunitiesPagination
+} from '../store/salesforceCrmSlice';
+
+const SalesforceDataScreen: React.FC = () => {
+ const { colors, fonts } = useTheme();
+ const dispatch = useDispatch();
+ const [selectedTab, setSelectedTab] = useState<'events' | 'leads' | 'tasks' | 'accounts' | 'opportunities'>('leads');
+ const [refreshing, setRefreshing] = useState(false);
+
+ // Redux selectors
+ const events = useSelector(selectSalesforceEvents);
+ const leads = useSelector(selectSalesforceLeads);
+ const tasks = useSelector(selectSalesforceTasks);
+ const accounts = useSelector(selectSalesforceAccounts);
+ const opportunities = useSelector(selectSalesforceOpportunities);
+ const loading = useSelector(selectSalesforceCrmLoading);
+ const errors = useSelector(selectSalesforceCrmErrors);
+
+ // Pagination selectors
+ const eventsPagination = useSelector(selectEventsPagination);
+ const leadsPagination = useSelector(selectLeadsPagination);
+ const tasksPagination = useSelector(selectTasksPagination);
+ const accountsPagination = useSelector(selectAccountsPagination);
+ const opportunitiesPagination = useSelector(selectOpportunitiesPagination);
+ const counts = useSelector(selectSalesforceCrmCounts);
+
+ // Fetch Salesforce CRM data
+ const fetchSalesforceData = useCallback(async (showRefresh = false) => {
+ try {
+ if (showRefresh) {
+ setRefreshing(true);
+ }
+
+ // Dispatch Redux action to fetch all Salesforce CRM data
+ await dispatch(fetchAllSalesforceCrmData()).unwrap();
+
+ if (showRefresh) {
+ showSuccess('Salesforce data refreshed successfully');
+ }
+ } catch (err) {
+ const errorMessage = 'Failed to fetch Salesforce data';
+ showError(errorMessage);
+ } finally {
+ setRefreshing(false);
+ }
+ }, [dispatch]);
+
+ // Load more data for infinite scrolling
+ const loadMoreData = useCallback(async (dataType: string) => {
+ try {
+ let pagination;
+ let fetchAction;
+
+ switch (dataType) {
+ case 'events':
+ pagination = eventsPagination;
+ fetchAction = fetchSalesforceEvents;
+ break;
+ case 'leads':
+ pagination = leadsPagination;
+ fetchAction = fetchSalesforceLeads;
+ break;
+ case 'tasks':
+ pagination = tasksPagination;
+ fetchAction = fetchSalesforceTasks;
+ break;
+ case 'accounts':
+ pagination = accountsPagination;
+ fetchAction = fetchSalesforceAccounts;
+ break;
+ case 'opportunities':
+ pagination = opportunitiesPagination;
+ fetchAction = fetchSalesforceOpportunities;
+ break;
+ default:
+ return;
+ }
+
+ // Check if there are more records and not currently loading
+ if (!pagination.moreRecords || loading[dataType as keyof typeof loading]) {
+ return;
+ }
+
+ // Fetch next page
+ await (dispatch(fetchAction({
+ page: pagination.page + 1,
+ limit: 20,
+ append: true
+ }) as any)).unwrap();
+
+ } catch (err) {
+ showError(`Failed to load more ${dataType}`);
+ }
+ }, [dispatch, eventsPagination, leadsPagination, tasksPagination, accountsPagination, opportunitiesPagination, loading]);
+
+ useEffect(() => {
+ fetchSalesforceData();
+ // Fetch counts in parallel
+ dispatch(fetchSalesforceCrmCounts());
+ }, []);
+
+ const handleRefresh = useCallback(() => {
+ // Reset pagination for all data types before refreshing
+ dispatch(resetEventsPagination());
+ dispatch(resetLeadsPagination());
+ dispatch(resetTasksPagination());
+ dispatch(resetAccountsPagination());
+ dispatch(resetOpportunitiesPagination());
+
+ // Then fetch fresh data and counts
+ fetchSalesforceData(true);
+ dispatch(fetchSalesforceCrmCounts());
+ }, [fetchSalesforceData, dispatch]);
+
+ const handleRetry = useCallback(() => {
+ fetchSalesforceData();
+ }, [fetchSalesforceData]);
+
+ const handleCardPress = useCallback((item: any, type: string) => {
+ showInfo(`Viewing ${type}: ${item.Name || item.Subject || `${item.FirstName} ${item.LastName}`}`);
+ }, []);
+
+ // Render loading footer for infinite scroll
+ const renderFooter = useCallback((dataType: string) => {
+ const isLoadingMore = loading[dataType as keyof typeof loading];
+ if (!isLoadingMore) return null;
+
+ return (
+
+
+
+ Loading more...
+
+
+ );
+ }, [loading, colors]);
+
+ // Get current loading state and error
+ const isLoading = loading.events || loading.leads || loading.tasks || loading.accounts || loading.opportunities;
+ const hasError = errors.events || errors.leads || errors.tasks || errors.accounts || errors.opportunities;
+
+ // Tab configuration with counts from API
+ const tabs = [
+ {
+ key: 'events',
+ label: 'Events',
+ icon: 'calendar',
+ count: counts?.events || events.length
+ },
+ {
+ key: 'leads',
+ label: 'Leads',
+ icon: 'account-heart',
+ count: counts?.leads || leads.length
+ },
+ {
+ key: 'tasks',
+ label: 'Tasks',
+ icon: 'check-circle',
+ count: counts?.tasks || tasks.length
+ },
+ {
+ key: 'accounts',
+ label: 'Accounts',
+ icon: 'domain',
+ count: counts?.accounts || accounts.length
+ },
+ {
+ key: 'opportunities',
+ label: 'Opportunities',
+ icon: 'handshake',
+ count: counts?.opportunities || opportunities.length
+ },
+ ] as const;
+
+ const renderTabContent = useCallback(() => {
+ const commonFlatListProps = {
+ numColumns: 1,
+ showsVerticalScrollIndicator: false,
+ contentContainerStyle: styles.listContainer,
+ refreshControl: (
+
+ ),
+ };
+
+ switch (selectedTab) {
+ case 'events':
+ return (
+ (
+ handleCardPress(item, 'Event')}
+ />
+ )}
+ keyExtractor={(item) => `event-${item.Id}`}
+ onEndReached={() => loadMoreData('events')}
+ onEndReachedThreshold={0.1}
+ ListFooterComponent={() => renderFooter('events')}
+ {...commonFlatListProps}
+ />
+ );
+ case 'leads':
+ return (
+ (
+ handleCardPress(item, 'Lead')}
+ />
+ )}
+ keyExtractor={(item) => `lead-${item.Id}`}
+ onEndReached={() => loadMoreData('leads')}
+ onEndReachedThreshold={0.1}
+ ListFooterComponent={() => renderFooter('leads')}
+ {...commonFlatListProps}
+ />
+ );
+ case 'tasks':
+ return (
+ (
+ handleCardPress(item, 'Task')}
+ />
+ )}
+ keyExtractor={(item) => `task-${item.Id}`}
+ onEndReached={() => loadMoreData('tasks')}
+ onEndReachedThreshold={0.1}
+ ListFooterComponent={() => renderFooter('tasks')}
+ {...commonFlatListProps}
+ />
+ );
+ case 'accounts':
+ return (
+ (
+ handleCardPress(item, 'Account')}
+ />
+ )}
+ keyExtractor={(item) => `account-${item.Id}`}
+ onEndReached={() => loadMoreData('accounts')}
+ onEndReachedThreshold={0.1}
+ ListFooterComponent={() => renderFooter('accounts')}
+ {...commonFlatListProps}
+ />
+ );
+ case 'opportunities':
+ return (
+ (
+ handleCardPress(item, 'Opportunity')}
+ />
+ )}
+ keyExtractor={(item) => `opportunity-${item.Id}`}
+ onEndReached={() => loadMoreData('opportunities')}
+ onEndReachedThreshold={0.1}
+ ListFooterComponent={() => renderFooter('opportunities')}
+ {...commonFlatListProps}
+ />
+ );
+ default:
+ return null;
+ }
+ }, [selectedTab, events, leads, tasks, accounts, opportunities, handleCardPress, loadMoreData, renderFooter, refreshing, handleRefresh]);
+
+ // Conditional returns after all hooks
+ if (isLoading && !leads.length) {
+ return ;
+ }
+
+ if (hasError && !leads.length) {
+ return ;
+ }
+
+ return (
+
+ {/* Fixed Header */}
+
+
+ Salesforce CRM Data
+
+
+
+
+
+
+ {/* Fixed Tabs */}
+
+
+
+ {tabs.map((tab) => (
+ setSelectedTab(tab.key)}
+ activeOpacity={0.8}
+ >
+
+
+ {tab.label}
+
+
+
+ {tab.count}
+
+
+
+ ))}
+
+
+
+
+ {/* Scrollable Content */}
+
+ {renderTabContent()}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ title: {
+ fontSize: 24,
+ },
+ tabsContainer: {
+ backgroundColor: '#FFFFFF',
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: '#E5E7EB',
+ },
+ tabs: {
+ flexDirection: 'row',
+ paddingHorizontal: 16,
+ },
+ tab: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ marginRight: 8,
+ borderRadius: 20,
+ backgroundColor: '#F1F5F9',
+ },
+ tabText: {
+ marginLeft: 6,
+ fontSize: 14,
+ },
+ countBadge: {
+ marginLeft: 6,
+ paddingHorizontal: 8,
+ paddingVertical: 2,
+ borderRadius: 10,
+ minWidth: 20,
+ alignItems: 'center',
+ },
+ countText: {
+ fontSize: 12,
+ },
+ content: {
+ flex: 1,
+ },
+ listContainer: {
+ paddingHorizontal: 16,
+ paddingBottom: 20,
+ },
+ footerLoader: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingVertical: 16,
+ },
+ footerText: {
+ marginLeft: 8,
+ fontSize: 14,
+ },
+});
+
+export default SalesforceDataScreen;
+
diff --git a/src/modules/crm/salesforce/services/salesforceCrmAPI.ts b/src/modules/crm/salesforce/services/salesforceCrmAPI.ts
new file mode 100644
index 0000000..bff424a
--- /dev/null
+++ b/src/modules/crm/salesforce/services/salesforceCrmAPI.ts
@@ -0,0 +1,65 @@
+import http from '@/services/http';
+import type {
+ SalesforceEvent,
+ SalesforceLead,
+ SalesforceTask,
+ SalesforceAccount,
+ SalesforceOpportunity,
+ SalesforceSearchParams,
+ SalesforceApiResponse
+} from '../types/SalesforceCrmTypes';
+
+// Available Salesforce CRM resource types
+export type SalesforceResourceType = 'events' | 'leads' | 'tasks' | 'accounts' | 'opportunities';
+
+// Base API endpoint for Salesforce CRM
+const SALESFORCE_CRM_BASE_URL = '/api/v1/n8n/salesforce/crm';
+
+export const salesforceCrmAPI = {
+ // Generic method to get Salesforce CRM data by resource type
+ getSalesforceCrmData: (
+ resource: SalesforceResourceType,
+ params?: SalesforceSearchParams
+ ) => {
+ const queryParams = {
+ page: params?.page || 1,
+ limit: params?.limit || 20,
+ ...params
+ };
+
+ return http.get>(`${SALESFORCE_CRM_BASE_URL}/${resource}`, queryParams);
+ },
+
+ // Specific resource methods for type safety
+ getEvents: (params?: SalesforceSearchParams) =>
+ salesforceCrmAPI.getSalesforceCrmData('events', params),
+
+ getLeads: (params?: SalesforceSearchParams) =>
+ salesforceCrmAPI.getSalesforceCrmData('leads', params),
+
+ getTasks: (params?: SalesforceSearchParams) =>
+ salesforceCrmAPI.getSalesforceCrmData('tasks', params),
+
+ getAccounts: (params?: SalesforceSearchParams) =>
+ salesforceCrmAPI.getSalesforceCrmData('accounts', params),
+
+ getOpportunities: (params?: SalesforceSearchParams) =>
+ salesforceCrmAPI.getSalesforceCrmData('opportunities', params),
+
+ // Get counts for all Salesforce CRM modules
+ getSalesforceCrmCounts: () => {
+ return http.get<{
+ status: string;
+ message: string;
+ data: {
+ events: { count: number; success: boolean };
+ leads: { count: number; success: boolean };
+ tasks: { count: number; success: boolean };
+ accounts: { count: number; success: boolean };
+ opportunities: { count: number; success: boolean };
+ };
+ timestamp: string;
+ }>(`${SALESFORCE_CRM_BASE_URL}/counts`);
+ },
+};
+
diff --git a/src/modules/crm/salesforce/store/salesforceCrmSlice.ts b/src/modules/crm/salesforce/store/salesforceCrmSlice.ts
new file mode 100644
index 0000000..d80a2ec
--- /dev/null
+++ b/src/modules/crm/salesforce/store/salesforceCrmSlice.ts
@@ -0,0 +1,517 @@
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import { salesforceCrmAPI } from '../services/salesforceCrmAPI';
+import type {
+ SalesforceEvent,
+ SalesforceLead,
+ SalesforceTask,
+ SalesforceAccount,
+ SalesforceOpportunity,
+ SalesforceStats,
+ SalesforceSearchParams
+} from '../types/SalesforceCrmTypes';
+
+// State interface
+export interface SalesforceCrmState {
+ // Data
+ events: SalesforceEvent[];
+ leads: SalesforceLead[];
+ tasks: SalesforceTask[];
+ accounts: SalesforceAccount[];
+ opportunities: SalesforceOpportunity[];
+
+ // Loading states
+ loading: {
+ events: boolean;
+ leads: boolean;
+ tasks: boolean;
+ accounts: boolean;
+ opportunities: boolean;
+ stats: boolean;
+ };
+
+ // Error states
+ errors: {
+ events: string | null;
+ leads: string | null;
+ tasks: string | null;
+ accounts: string | null;
+ opportunities: string | null;
+ stats: string | null;
+ };
+
+ // Pagination
+ pagination: {
+ events: { page: number; count: number; moreRecords: boolean };
+ leads: { page: number; count: number; moreRecords: boolean };
+ tasks: { page: number; count: number; moreRecords: boolean };
+ accounts: { page: number; count: number; moreRecords: boolean };
+ opportunities: { page: number; count: number; moreRecords: boolean };
+ };
+
+ // Statistics
+ stats: SalesforceStats | null;
+
+ // Module counts
+ counts: {
+ events: number;
+ leads: number;
+ tasks: number;
+ accounts: number;
+ opportunities: number;
+ } | null;
+
+ // Last updated timestamps
+ lastUpdated: {
+ events: string | null;
+ leads: string | null;
+ tasks: string | null;
+ accounts: string | null;
+ opportunities: string | null;
+ stats: string | null;
+ };
+}
+
+// Initial state
+const initialState: SalesforceCrmState = {
+ events: [],
+ leads: [],
+ tasks: [],
+ accounts: [],
+ opportunities: [],
+ loading: {
+ events: false,
+ leads: false,
+ tasks: false,
+ accounts: false,
+ opportunities: false,
+ stats: false,
+ },
+ errors: {
+ events: null,
+ leads: null,
+ tasks: null,
+ accounts: null,
+ opportunities: null,
+ stats: null,
+ },
+ pagination: {
+ events: { page: 1, count: 0, moreRecords: false },
+ leads: { page: 1, count: 0, moreRecords: false },
+ tasks: { page: 1, count: 0, moreRecords: false },
+ accounts: { page: 1, count: 0, moreRecords: false },
+ opportunities: { page: 1, count: 0, moreRecords: false },
+ },
+ stats: null,
+ counts: null,
+ lastUpdated: {
+ events: null,
+ leads: null,
+ tasks: null,
+ accounts: null,
+ opportunities: null,
+ stats: null,
+ },
+};
+
+// Async thunks for infinite scrolling
+export const fetchSalesforceEvents = createAsyncThunk(
+ 'salesforceCrm/fetchEvents',
+ async (params?: SalesforceSearchParams & { append?: boolean }) => {
+ const response = await salesforceCrmAPI.getEvents(params);
+ return {
+ data: response.data?.data?.data || [],
+ count: response.data?.data?.count || 0,
+ moreRecords: !response.data?.data?.metadata?.done,
+ append: params?.append || false
+ };
+ }
+);
+
+export const fetchSalesforceLeads = createAsyncThunk(
+ 'salesforceCrm/fetchLeads',
+ async (params?: SalesforceSearchParams & { append?: boolean }) => {
+ const response = await salesforceCrmAPI.getLeads(params);
+ return {
+ data: response.data?.data?.data || [],
+ count: response.data?.data?.count || 0,
+ moreRecords: !response.data?.data?.metadata?.done,
+ append: params?.append || false
+ };
+ }
+);
+
+export const fetchSalesforceTasks = createAsyncThunk(
+ 'salesforceCrm/fetchTasks',
+ async (params?: SalesforceSearchParams & { append?: boolean }) => {
+ const response = await salesforceCrmAPI.getTasks(params);
+ return {
+ data: response.data?.data?.data || [],
+ count: response.data?.data?.count || 0,
+ moreRecords: !response.data?.data?.metadata?.done,
+ append: params?.append || false
+ };
+ }
+);
+
+export const fetchSalesforceAccounts = createAsyncThunk(
+ 'salesforceCrm/fetchAccounts',
+ async (params?: SalesforceSearchParams & { append?: boolean }) => {
+ const response = await salesforceCrmAPI.getAccounts(params);
+ return {
+ data: response.data?.data?.data || [],
+ count: response.data?.data?.count || 0,
+ moreRecords: !response.data?.data?.metadata?.done,
+ append: params?.append || false
+ };
+ }
+);
+
+export const fetchSalesforceOpportunities = createAsyncThunk(
+ 'salesforceCrm/fetchOpportunities',
+ async (params?: SalesforceSearchParams & { append?: boolean }) => {
+ const response = await salesforceCrmAPI.getOpportunities(params);
+ return {
+ data: response.data?.data?.data || [],
+ count: response.data?.data?.count || 0,
+ moreRecords: !response.data?.data?.metadata?.done,
+ append: params?.append || false
+ };
+ }
+);
+
+// Fetch Salesforce CRM counts
+export const fetchSalesforceCrmCounts = createAsyncThunk(
+ 'salesforceCrm/fetchCounts',
+ async () => {
+ const response = await salesforceCrmAPI.getSalesforceCrmCounts();
+ return response.data || {
+ events: { count: 0, success: false },
+ leads: { count: 0, success: false },
+ tasks: { count: 0, success: false },
+ accounts: { count: 0, success: false },
+ opportunities: { count: 0, success: false },
+ };
+ }
+);
+
+// Fetch all Salesforce CRM data
+export const fetchAllSalesforceCrmData = createAsyncThunk(
+ 'salesforceCrm/fetchAllData',
+ async (params?: SalesforceSearchParams) => {
+ const [
+ eventsResponse,
+ leadsResponse,
+ tasksResponse,
+ accountsResponse,
+ opportunitiesResponse
+ ] = await Promise.all([
+ salesforceCrmAPI.getEvents(params),
+ salesforceCrmAPI.getLeads(params),
+ salesforceCrmAPI.getTasks(params),
+ salesforceCrmAPI.getAccounts(params),
+ salesforceCrmAPI.getOpportunities(params),
+ ]);
+
+ return {
+ events: {
+ data: eventsResponse.data?.data?.data || [],
+ count: eventsResponse.data?.data?.count || 0,
+ moreRecords: !eventsResponse.data?.data?.metadata?.done
+ },
+ leads: {
+ data: leadsResponse.data?.data?.data || [],
+ count: leadsResponse.data?.data?.count || 0,
+ moreRecords: !leadsResponse.data?.data?.metadata?.done
+ },
+ tasks: {
+ data: tasksResponse.data?.data?.data || [],
+ count: tasksResponse.data?.data?.count || 0,
+ moreRecords: !tasksResponse.data?.data?.metadata?.done
+ },
+ accounts: {
+ data: accountsResponse.data?.data?.data || [],
+ count: accountsResponse.data?.data?.count || 0,
+ moreRecords: !accountsResponse.data?.data?.metadata?.done
+ },
+ opportunities: {
+ data: opportunitiesResponse.data?.data?.data || [],
+ count: opportunitiesResponse.data?.data?.count || 0,
+ moreRecords: !opportunitiesResponse.data?.data?.metadata?.done
+ },
+ };
+ }
+);
+
+// Slice
+const salesforceCrmSlice = createSlice({
+ name: 'salesforceCrm',
+ initialState,
+ reducers: {
+ clearErrors: (state) => {
+ state.errors = {
+ events: null,
+ leads: null,
+ tasks: null,
+ accounts: null,
+ opportunities: null,
+ stats: null,
+ };
+ },
+ clearData: (state) => {
+ state.events = [];
+ state.leads = [];
+ state.tasks = [];
+ state.accounts = [];
+ state.opportunities = [];
+ state.stats = null;
+ },
+ // Reset pagination for specific data types
+ resetEventsPagination: (state) => {
+ state.pagination.events = { page: 1, count: 0, moreRecords: false };
+ state.events = [];
+ },
+ resetLeadsPagination: (state) => {
+ state.pagination.leads = { page: 1, count: 0, moreRecords: false };
+ state.leads = [];
+ },
+ resetTasksPagination: (state) => {
+ state.pagination.tasks = { page: 1, count: 0, moreRecords: false };
+ state.tasks = [];
+ },
+ resetAccountsPagination: (state) => {
+ state.pagination.accounts = { page: 1, count: 0, moreRecords: false };
+ state.accounts = [];
+ },
+ resetOpportunitiesPagination: (state) => {
+ state.pagination.opportunities = { page: 1, count: 0, moreRecords: false };
+ state.opportunities = [];
+ },
+ },
+ extraReducers: (builder) => {
+ // Fetch events
+ builder
+ .addCase(fetchSalesforceEvents.pending, (state) => {
+ state.loading.events = true;
+ state.errors.events = null;
+ })
+ .addCase(fetchSalesforceEvents.fulfilled, (state, action) => {
+ state.loading.events = false;
+ const { data, count, moreRecords, append } = action.payload;
+
+ if (append) {
+ // Append new data for infinite scrolling
+ state.events = [...state.events, ...data];
+ } else {
+ // Replace data for initial load or refresh
+ state.events = data;
+ }
+
+ state.pagination.events = {
+ page: append ? state.pagination.events.page + 1 : 1,
+ count,
+ moreRecords
+ };
+ state.lastUpdated.events = new Date().toISOString();
+ })
+ .addCase(fetchSalesforceEvents.rejected, (state, action) => {
+ state.loading.events = false;
+ state.errors.events = action.error.message || 'Failed to fetch events';
+ })
+
+ // Fetch leads
+ .addCase(fetchSalesforceLeads.pending, (state) => {
+ state.loading.leads = true;
+ state.errors.leads = null;
+ })
+ .addCase(fetchSalesforceLeads.fulfilled, (state, action) => {
+ state.loading.leads = false;
+ const { data, count, moreRecords, append } = action.payload;
+
+ if (append) {
+ state.leads = [...state.leads, ...data];
+ } else {
+ state.leads = data;
+ }
+
+ state.pagination.leads = {
+ page: append ? state.pagination.leads.page + 1 : 1,
+ count,
+ moreRecords
+ };
+ state.lastUpdated.leads = new Date().toISOString();
+ })
+ .addCase(fetchSalesforceLeads.rejected, (state, action) => {
+ state.loading.leads = false;
+ state.errors.leads = action.error.message || 'Failed to fetch leads';
+ })
+
+ // Fetch tasks
+ .addCase(fetchSalesforceTasks.pending, (state) => {
+ state.loading.tasks = true;
+ state.errors.tasks = null;
+ })
+ .addCase(fetchSalesforceTasks.fulfilled, (state, action) => {
+ state.loading.tasks = false;
+ const { data, count, moreRecords, append } = action.payload;
+
+ if (append) {
+ state.tasks = [...state.tasks, ...data];
+ } else {
+ state.tasks = data;
+ }
+
+ state.pagination.tasks = {
+ page: append ? state.pagination.tasks.page + 1 : 1,
+ count,
+ moreRecords
+ };
+ state.lastUpdated.tasks = new Date().toISOString();
+ })
+ .addCase(fetchSalesforceTasks.rejected, (state, action) => {
+ state.loading.tasks = false;
+ state.errors.tasks = action.error.message || 'Failed to fetch tasks';
+ })
+
+ // Fetch accounts
+ .addCase(fetchSalesforceAccounts.pending, (state) => {
+ state.loading.accounts = true;
+ state.errors.accounts = null;
+ })
+ .addCase(fetchSalesforceAccounts.fulfilled, (state, action) => {
+ state.loading.accounts = false;
+ const { data, count, moreRecords, append } = action.payload;
+
+ if (append) {
+ state.accounts = [...state.accounts, ...data];
+ } else {
+ state.accounts = data;
+ }
+
+ state.pagination.accounts = {
+ page: append ? state.pagination.accounts.page + 1 : 1,
+ count,
+ moreRecords
+ };
+ state.lastUpdated.accounts = new Date().toISOString();
+ })
+ .addCase(fetchSalesforceAccounts.rejected, (state, action) => {
+ state.loading.accounts = false;
+ state.errors.accounts = action.error.message || 'Failed to fetch accounts';
+ })
+
+ // Fetch opportunities
+ .addCase(fetchSalesforceOpportunities.pending, (state) => {
+ state.loading.opportunities = true;
+ state.errors.opportunities = null;
+ })
+ .addCase(fetchSalesforceOpportunities.fulfilled, (state, action) => {
+ state.loading.opportunities = false;
+ const { data, count, moreRecords, append } = action.payload;
+
+ if (append) {
+ state.opportunities = [...state.opportunities, ...data];
+ } else {
+ state.opportunities = data;
+ }
+
+ state.pagination.opportunities = {
+ page: append ? state.pagination.opportunities.page + 1 : 1,
+ count,
+ moreRecords
+ };
+ state.lastUpdated.opportunities = new Date().toISOString();
+ })
+ .addCase(fetchSalesforceOpportunities.rejected, (state, action) => {
+ state.loading.opportunities = false;
+ state.errors.opportunities = action.error.message || 'Failed to fetch opportunities';
+ })
+
+ // Fetch all Salesforce CRM data
+ .addCase(fetchAllSalesforceCrmData.pending, (state) => {
+ state.loading.events = true;
+ state.loading.leads = true;
+ state.loading.tasks = true;
+ state.loading.accounts = true;
+ state.loading.opportunities = true;
+ state.errors.events = null;
+ state.errors.leads = null;
+ state.errors.tasks = null;
+ state.errors.accounts = null;
+ state.errors.opportunities = null;
+ })
+ .addCase(fetchAllSalesforceCrmData.fulfilled, (state, action) => {
+ const { events, leads, tasks, accounts, opportunities } = action.payload;
+
+ state.loading.events = false;
+ state.loading.leads = false;
+ state.loading.tasks = false;
+ state.loading.accounts = false;
+ state.loading.opportunities = false;
+
+ state.events = events.data;
+ state.leads = leads.data;
+ state.tasks = tasks.data;
+ state.accounts = accounts.data;
+ state.opportunities = opportunities.data;
+
+ state.pagination.events = { page: 1, count: events.count, moreRecords: events.moreRecords };
+ state.pagination.leads = { page: 1, count: leads.count, moreRecords: leads.moreRecords };
+ state.pagination.tasks = { page: 1, count: tasks.count, moreRecords: tasks.moreRecords };
+ state.pagination.accounts = { page: 1, count: accounts.count, moreRecords: accounts.moreRecords };
+ state.pagination.opportunities = { page: 1, count: opportunities.count, moreRecords: opportunities.moreRecords };
+
+ const now = new Date().toISOString();
+ state.lastUpdated.events = now;
+ state.lastUpdated.leads = now;
+ state.lastUpdated.tasks = now;
+ state.lastUpdated.accounts = now;
+ state.lastUpdated.opportunities = now;
+ })
+ .addCase(fetchAllSalesforceCrmData.rejected, (state, action) => {
+ state.loading.events = false;
+ state.loading.leads = false;
+ state.loading.tasks = false;
+ state.loading.accounts = false;
+ state.loading.opportunities = false;
+
+ const errorMessage = action.error.message || 'Failed to fetch Salesforce CRM data';
+ state.errors.events = errorMessage;
+ state.errors.leads = errorMessage;
+ state.errors.tasks = errorMessage;
+ state.errors.accounts = errorMessage;
+ state.errors.opportunities = errorMessage;
+ })
+
+ // Fetch Salesforce CRM counts
+ .addCase(fetchSalesforceCrmCounts.pending, (state) => {
+ // No loading state needed for counts as it's fetched in background
+ })
+ .addCase(fetchSalesforceCrmCounts.fulfilled, (state, action) => {
+ const data = action.payload as any;
+ state.counts = {
+ events: data.events?.count || 0,
+ leads: data.leads?.count || 0,
+ tasks: data.tasks?.count || 0,
+ accounts: data.accounts?.count || 0,
+ opportunities: data.opportunities?.count || 0,
+ };
+ })
+ .addCase(fetchSalesforceCrmCounts.rejected, (state) => {
+ // Keep existing counts on error
+ console.warn('Failed to fetch Salesforce CRM counts');
+ });
+ },
+});
+
+export const {
+ clearErrors,
+ clearData,
+ resetEventsPagination,
+ resetLeadsPagination,
+ resetTasksPagination,
+ resetAccountsPagination,
+ resetOpportunitiesPagination,
+} = salesforceCrmSlice.actions;
+
+export default salesforceCrmSlice.reducer;
+
diff --git a/src/modules/crm/salesforce/store/selectors.ts b/src/modules/crm/salesforce/store/selectors.ts
new file mode 100644
index 0000000..284c668
--- /dev/null
+++ b/src/modules/crm/salesforce/store/selectors.ts
@@ -0,0 +1,150 @@
+import { createSelector } from '@reduxjs/toolkit';
+import type { RootState } from '@/store/store';
+import type { SalesforceCrmState } from './salesforceCrmSlice';
+
+// Base selector
+const selectSalesforceCrmState = (state: RootState): SalesforceCrmState =>
+ state.salesforceCrm as SalesforceCrmState;
+
+// Data selectors
+export const selectSalesforceEvents = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.events
+);
+
+export const selectSalesforceLeads = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.leads
+);
+
+export const selectSalesforceTasks = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.tasks
+);
+
+export const selectSalesforceAccounts = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.accounts
+);
+
+export const selectSalesforceOpportunities = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.opportunities
+);
+
+// Loading selectors
+export const selectSalesforceCrmLoading = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.loading
+);
+
+export const selectIsAnyLoading = createSelector(
+ [selectSalesforceCrmLoading],
+ (loading) => Object.values(loading).some(isLoading => isLoading)
+);
+
+// Error selectors
+export const selectSalesforceCrmErrors = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.errors
+);
+
+export const selectHasAnyError = createSelector(
+ [selectSalesforceCrmErrors],
+ (errors) => Object.values(errors).some(error => error !== null)
+);
+
+// Pagination selectors
+export const selectEventsPagination = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.pagination.events
+);
+
+export const selectLeadsPagination = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.pagination.leads
+);
+
+export const selectTasksPagination = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.pagination.tasks
+);
+
+export const selectAccountsPagination = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.pagination.accounts
+);
+
+export const selectOpportunitiesPagination = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.pagination.opportunities
+);
+
+// Count selectors
+export const selectSalesforceCrmCounts = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.counts
+);
+
+// Stats selectors
+export const selectSalesforceCrmStats = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.stats
+);
+
+// Dashboard data selector - combines all data for dashboard view
+export const selectDashboardData = createSelector(
+ [
+ selectSalesforceEvents,
+ selectSalesforceLeads,
+ selectSalesforceTasks,
+ selectSalesforceAccounts,
+ selectSalesforceOpportunities,
+ selectSalesforceCrmCounts,
+ ],
+ (events, leads, tasks, accounts, opportunities, counts) => ({
+ events,
+ leads,
+ tasks,
+ accounts,
+ opportunities,
+ counts,
+ summary: {
+ totalEvents: events.length,
+ totalLeads: leads.length,
+ totalTasks: tasks.length,
+ totalAccounts: accounts.length,
+ totalOpportunities: opportunities.length,
+
+ // Event stats
+ upcomingEvents: events.filter(e => e.StartDateTime && new Date(e.StartDateTime) > new Date()).length,
+
+ // Lead stats
+ newLeads: leads.filter(l => l.Status === 'New').length,
+ workingLeads: leads.filter(l => l.Status === 'Working').length,
+ qualifiedLeads: leads.filter(l => l.Status === 'Qualified').length,
+
+ // Task stats
+ openTasks: tasks.filter(t => t.Status === 'Open').length,
+ completedTasks: tasks.filter(t => t.Status === 'Completed').length,
+ highPriorityTasks: tasks.filter(t => t.Priority === 'High').length,
+
+ // Opportunity stats
+ openOpportunities: opportunities.filter(o => !o.IsClosed).length,
+ totalPipelineValue: opportunities
+ .filter(o => !o.IsClosed)
+ .reduce((sum, o) => sum + (o.Amount || 0), 0),
+ averageDealSize: opportunities.length > 0
+ ? opportunities.reduce((sum, o) => sum + (o.Amount || 0), 0) / opportunities.length
+ : 0,
+ wonOpportunities: opportunities.filter(o => o.IsWon).length,
+ },
+ })
+);
+
+// Last updated selector
+export const selectLastUpdated = createSelector(
+ [selectSalesforceCrmState],
+ (state) => state.lastUpdated
+);
+
diff --git a/src/modules/crm/salesforce/types/SalesforceCrmTypes.ts b/src/modules/crm/salesforce/types/SalesforceCrmTypes.ts
new file mode 100644
index 0000000..f2d16f1
--- /dev/null
+++ b/src/modules/crm/salesforce/types/SalesforceCrmTypes.ts
@@ -0,0 +1,273 @@
+// Salesforce CRM Data Types for Integration
+
+export interface SalesforceEvent {
+ Id: string;
+ Subject: string;
+ StartDateTime: string | null;
+ EndDateTime: string | null;
+ Location: string;
+ Description: string | null;
+ OwnerId: string;
+ WhatId: string | null;
+ WhoId: string | null;
+ IsAllDayEvent: boolean;
+ CreatedDate: string;
+ attributes: {
+ type: string;
+ url: string;
+ };
+}
+
+export interface SalesforceLead {
+ Id: string;
+ FirstName: string;
+ LastName: string;
+ Company: string;
+ Email: string;
+ Status: LeadStatus;
+ Phone?: string;
+ Title?: string;
+ Street?: string;
+ City?: string;
+ State?: string;
+ Country?: string;
+ PostalCode?: string;
+ Website?: string;
+ Industry?: string;
+ NumberOfEmployees?: number;
+ AnnualRevenue?: number;
+ OwnerId?: string;
+ LeadSource?: string;
+ Description?: string;
+ CreatedDate?: string;
+ LastModifiedDate?: string;
+ attributes: {
+ type: string;
+ url: string;
+ };
+}
+
+export interface SalesforceTask {
+ Id: string;
+ Subject: string;
+ Status: TaskStatus;
+ Priority: TaskPriority;
+ ActivityDate: string | null;
+ WhatId: string | null;
+ WhoId: string | null;
+ OwnerId: string;
+ Description: string | null;
+ CreatedDate: string;
+ LastModifiedDate: string;
+ IsClosed?: boolean;
+ IsHighPriority?: boolean;
+ attributes: {
+ type: string;
+ url: string;
+ };
+}
+
+export interface SalesforceAccount {
+ Id: string;
+ Name: string;
+ Industry: string | null;
+ Type: string | null;
+ Phone: string | null;
+ BillingCity: string | null;
+ BillingState: string | null;
+ BillingCountry: string | null;
+ BillingStreet?: string | null;
+ BillingPostalCode?: string | null;
+ ShippingCity?: string | null;
+ ShippingState?: string | null;
+ ShippingCountry?: string | null;
+ ShippingStreet?: string | null;
+ ShippingPostalCode?: string | null;
+ Website: string | null;
+ OwnerId: string;
+ CreatedDate: string;
+ LastModifiedDate?: string;
+ Description?: string | null;
+ NumberOfEmployees?: number;
+ AnnualRevenue?: number;
+ attributes: {
+ type: string;
+ url: string;
+ };
+}
+
+export interface SalesforceOpportunity {
+ Id: string;
+ Name: string;
+ StageName: OpportunityStage;
+ Amount: number;
+ CloseDate: string;
+ AccountId: string | null;
+ Type: string | null;
+ Probability: number;
+ ForecastCategory: string;
+ OwnerId: string;
+ CreatedDate: string;
+ LastModifiedDate: string;
+ Description?: string | null;
+ LeadSource?: string | null;
+ NextStep?: string | null;
+ IsClosed?: boolean;
+ IsWon?: boolean;
+ ExpectedRevenue?: number;
+ attributes: {
+ type: string;
+ url: string;
+ };
+}
+
+// Enums and Union Types
+export type LeadStatus = 'New' | 'Working' | 'Nurturing' | 'Qualified' | 'Unqualified';
+export type TaskStatus = 'Open' | 'In Progress' | 'Completed' | 'Waiting' | 'Deferred';
+export type TaskPriority = 'High' | 'Normal' | 'Low';
+export type OpportunityStage =
+ | 'Prospecting'
+ | 'Qualification'
+ | 'Needs Analysis'
+ | 'Value Proposition'
+ | 'Identify Decision Makers'
+ | 'Perception Analysis'
+ | 'Proposal/Price Quote'
+ | 'Negotiation/Review'
+ | 'Closed Won'
+ | 'Closed Lost';
+
+// Combined Salesforce CRM Data Interface
+export interface SalesforceCrmData {
+ events: SalesforceEvent[];
+ leads: SalesforceLead[];
+ tasks: SalesforceTask[];
+ accounts: SalesforceAccount[];
+ opportunities: SalesforceOpportunity[];
+}
+
+// API Response Types
+export interface SalesforceApiResponse {
+ status: 'success' | 'error';
+ message: string;
+ data?: {
+ success: boolean;
+ data: T[];
+ count: number;
+ metadata: {
+ totalSize: number;
+ done: boolean;
+ nextRecordsUrl: string | null;
+ };
+ };
+ error?: string;
+ timestamp: string;
+}
+
+export interface SalesforcePaginatedResponse {
+ data: T[];
+ info: {
+ count: number;
+ moreRecords: boolean;
+ page: number;
+ };
+}
+
+// Filter and Search Types
+export interface SalesforceFilters {
+ events?: {
+ dateRange?: {
+ start: string;
+ end: string;
+ };
+ };
+ leads?: {
+ status?: LeadStatus[];
+ source?: string[];
+ assignedTo?: string[];
+ dateRange?: {
+ start: string;
+ end: string;
+ };
+ };
+ tasks?: {
+ status?: TaskStatus[];
+ priority?: TaskPriority[];
+ assignedTo?: string[];
+ dueDateRange?: {
+ start: string;
+ end: string;
+ };
+ };
+ accounts?: {
+ industry?: string[];
+ type?: string[];
+ assignedTo?: string[];
+ };
+ opportunities?: {
+ stage?: OpportunityStage[];
+ owner?: string[];
+ amountRange?: {
+ min: number;
+ max: number;
+ };
+ closeDateRange?: {
+ start: string;
+ end: string;
+ };
+ };
+}
+
+export interface SalesforceSearchParams {
+ query?: string;
+ type?: 'events' | 'leads' | 'tasks' | 'accounts' | 'opportunities' | 'all';
+ filters?: SalesforceFilters;
+ sortBy?: string;
+ sortOrder?: 'asc' | 'desc';
+ page?: number;
+ limit?: number;
+}
+
+// Statistics and Analytics Types
+export interface SalesforceStats {
+ events: {
+ total: number;
+ upcoming: number;
+ completed: number;
+ byLocation: Record;
+ };
+ leads: {
+ total: number;
+ new: number;
+ working: number;
+ qualified: number;
+ byStatus: Record;
+ bySource: Record;
+ };
+ tasks: {
+ total: number;
+ open: number;
+ completed: number;
+ overdue: number;
+ byStatus: Record;
+ byPriority: Record;
+ };
+ accounts: {
+ total: number;
+ byIndustry: Record;
+ byType: Record;
+ totalRevenue: number;
+ };
+ opportunities: {
+ total: number;
+ open: number;
+ won: number;
+ lost: number;
+ totalValue: number;
+ averageDealSize: number;
+ byStage: Record;
+ pipelineValue: number;
+ winRate: number;
+ };
+}
+
diff --git a/src/modules/integrations/screens/IntegrationCategoryScreen.tsx b/src/modules/integrations/screens/IntegrationCategoryScreen.tsx
index ecb641f..a5ab8d4 100644
--- a/src/modules/integrations/screens/IntegrationCategoryScreen.tsx
+++ b/src/modules/integrations/screens/IntegrationCategoryScreen.tsx
@@ -8,6 +8,7 @@ import { useDispatch } from 'react-redux';
import { setSelectedService } from '@/modules/integrations/store/integrationsSlice';
import type { AppDispatch } from '@/store/store';
import ZohoAuth from './ZohoAuth';
+import SalesforceAuth from './SalesforceAuth';
import { Modal } from 'react-native';
import httpClient from '@/services/http';
import { useNavigation } from '@react-navigation/native';
@@ -61,6 +62,7 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => {
const dispatch = useDispatch();
const navigation = useNavigation();
const [showZohoAuth, setShowZohoAuth] = React.useState(false);
+ const [showSalesforceAuth, setShowSalesforceAuth] = React.useState(false);
const [pendingService, setPendingService] = React.useState(null);
const [isCheckingToken, setIsCheckingToken] = React.useState(false);
const [authenticatedServices, setAuthenticatedServices] = React.useState>(new Set());
@@ -102,11 +104,50 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => {
}
};
+ // Check for existing Salesforce token
+ const checkSalesforceToken = async (serviceKey: string, forceReauth: boolean = false) => {
+ try {
+ setIsCheckingToken(true);
+
+ if (forceReauth) {
+ // Force re-authentication by showing auth modal
+ setPendingService(serviceKey);
+ setShowSalesforceAuth(true);
+ return;
+ }
+
+ const response = await httpClient.get('/api/v1/users/decrypt-token?service_name=salesforce');
+ const responseData = response.data as any;
+
+ if (responseData.status === 'success' && responseData.data?.accessToken) {
+ // Token exists and is valid, mark as authenticated
+ // CrmNavigator will automatically show SalesforceDashboard based on selectedService
+ console.log('Salesforce token found, setting selected service');
+ setAuthenticatedServices(prev => new Set([...prev, serviceKey]));
+ dispatch(setSelectedService(serviceKey));
+ } else {
+ // No valid token, show auth modal
+ setPendingService(serviceKey);
+ setShowSalesforceAuth(true);
+ }
+ } catch (error) {
+ console.log('No valid Salesforce token found, showing auth modal');
+ // Token doesn't exist or is invalid, show auth modal
+ setPendingService(serviceKey);
+ setShowSalesforceAuth(true);
+ } finally {
+ setIsCheckingToken(false);
+ }
+ };
+
// Handle re-authentication
const handleReAuthenticate = (serviceKey: string) => {
+ const isSalesforce = serviceKey === 'salesforce';
+ const serviceTitle = isSalesforce ? 'Salesforce' : 'Zoho';
+
Alert.alert(
'Re-authenticate',
- 'This will allow you to change your organization or re-authorize access. Continue?',
+ `This will allow you to change your ${serviceTitle} organization or re-authorize access. Continue?`,
[
{
text: 'Cancel',
@@ -114,7 +155,13 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => {
},
{
text: 'Re-authenticate',
- onPress: () => checkZohoToken(serviceKey, true),
+ onPress: () => {
+ if (isSalesforce) {
+ checkSalesforceToken(serviceKey, true);
+ } else {
+ checkZohoToken(serviceKey, true);
+ }
+ },
},
]
);
@@ -129,6 +176,8 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => {
ItemSeparatorComponent={() => }
renderItem={({ item }) => {
const requiresZohoAuth = item.key === 'zohoProjects' || item.key === 'zohoPeople' || item.key === 'zohoBooks' || item.key === 'zohoCRM';
+ const requiresSalesforceAuth = item.key === 'salesforce';
+ const requiresAuth = requiresZohoAuth || requiresSalesforceAuth;
return (
@@ -142,8 +191,10 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => {
if (requiresZohoAuth) {
checkZohoToken(item.key);
+ } else if (requiresSalesforceAuth) {
+ checkSalesforceToken(item.key);
} else {
- // For non-Zoho services, navigate to Coming Soon screen
+ // For other services, navigate to Coming Soon screen
navigation.navigate('ComingSoon' as never);
}
}}
@@ -161,8 +212,8 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => {
)}
- {/* Re-authentication button for Zoho services - always visible */}
- {requiresZohoAuth && (
+ {/* Re-authentication button for services that require auth */}
+ {requiresAuth && (
handleReAuthenticate(item.key)}
@@ -190,7 +241,7 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => {
{
- console.log('auth data i got',authData)
+ console.log('Zoho auth data received:', authData);
setShowZohoAuth(false);
if (pendingService) {
// Mark service as authenticated
@@ -209,6 +260,37 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => {
}}
/>
+
+ {/* Salesforce Auth Modal */}
+ setShowSalesforceAuth(false)}
+ >
+ {
+ console.log('Salesforce auth data received:', authData);
+ setShowSalesforceAuth(false);
+ if (pendingService) {
+ // Mark service as authenticated
+ // CrmNavigator will automatically show SalesforceDashboard based on selectedService
+ setAuthenticatedServices(prev => new Set([...prev, pendingService]));
+ dispatch(setSelectedService(pendingService));
+ setPendingService(null);
+ }
+ }}
+ onAuthError={(error) => {
+ console.error('Salesforce auth error:', error);
+ setShowSalesforceAuth(false);
+ setPendingService(null);
+ }}
+ onClose={() => {
+ setShowSalesforceAuth(false);
+ setPendingService(null);
+ }}
+ />
+
);
};
diff --git a/src/modules/integrations/screens/SALESFORCE_BACKEND_CALLBACK_FLOW.md b/src/modules/integrations/screens/SALESFORCE_BACKEND_CALLBACK_FLOW.md
new file mode 100644
index 0000000..138bff6
--- /dev/null
+++ b/src/modules/integrations/screens/SALESFORCE_BACKEND_CALLBACK_FLOW.md
@@ -0,0 +1,600 @@
+# Salesforce OAuth - Backend Callback Flow
+
+## Overview
+This document explains the backend-handled OAuth callback flow for Salesforce authentication, where the backend receives the authorization code directly from Salesforce and handles the token exchange.
+
+---
+
+## Architecture Flow
+
+```
+โโโโโโโโโโโโโโโโโโโ
+โ Mobile App โ
+โ (SalesforceAuth)โ
+โโโโโโโโโโฌโโโโโโโโโ
+ โ 1. User taps Salesforce
+ โ
+ โผ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ Build OAuth URL with backend callback: โ
+โ https://login.salesforce.com/services/oauth2/authorize โ
+โ ?client_id=YOUR_CLIENT_ID โ
+โ &redirect_uri=YOUR_BACKEND_URL/callback โ
+โ ?user_uuid=USER_ID&service_name=salesforce โ
+โ &response_type=code โ
+โ &scope=api refresh_token offline_access... โ
+โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ 2. Open in WebView
+ โผ
+ โโโโโโโโโโโโโโโโโโ
+ โ Salesforce โ
+ โ Login Page โ
+ โโโโโโโโโโฌโโโโโโโโ
+ โ 3. User authenticates
+ โ
+ โผ
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ Salesforce redirects to backend callback: โ
+ โ https://YOUR_BACKEND/callback โ
+ โ ?user_uuid=USER_ID โ
+ โ &service_name=salesforce โ
+ โ &code=AUTHORIZATION_CODE โ
+ โโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ 4. Backend receives callback
+ โผ
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ Your Backend Server โ
+ โ โ
+ โ โข Extract: authorization_code, user_uuid, โ
+ โ service_name from query params โ
+ โ โ
+ โ โข Exchange code for tokens with Salesforce:โ
+ โ POST /services/oauth2/token โ
+ โ grant_type=authorization_code โ
+ โ code=CODE โ
+ โ client_id=YOUR_CLIENT_ID โ
+ โ client_secret=YOUR_CLIENT_SECRET โ
+ โ redirect_uri=YOUR_CALLBACK_URL โ
+ โ โ
+ โ โข Store tokens (encrypted) in database โ
+ โ associated with user_uuid โ
+ โ โ
+ โ โข Return HTML page or redirect with โ
+ โ status indicator โ
+ โโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ 5. Backend responds
+ โ
+ โผ
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ Backend returns success/error page: โ
+ โ โ
+ โ Success: ?status=success โ
+ โ OR ?success=true โ
+ โ โ
+ โ Error: ?status=error โ
+ โ &message=Error+description โ
+ โโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ 6. WebView detects callback response
+ โผ
+ โโโโโโโโโโโโโโโโโโ
+ โ Mobile App โ
+ โ โข Detect backend callback URL โ
+ โ โข Check for status=success or error โ
+ โ โข Show success/error to user โ
+ โ โข Close auth modal โ
+ โโโโโโโโโโโโโโโโโโ
+```
+
+---
+
+## Backend Implementation
+
+### Endpoint: `GET /api/v1/users/oauth/callback`
+
+#### Query Parameters (Received from Salesforce)
+```typescript
+{
+ user_uuid: string; // User ID passed in redirect_uri
+ service_name: string; // 'salesforce' passed in redirect_uri
+ code: string; // Authorization code from Salesforce
+ state?: string; // Optional CSRF token
+}
+```
+
+#### Implementation Steps
+
+##### 1. Extract Query Parameters
+```javascript
+const { code: authorization_code, user_uuid, service_name } = req.query;
+
+// Validate required parameters
+if (!authorization_code || !user_uuid || !service_name) {
+ return res.redirect(`/oauth/error?message=Missing required parameters`);
+}
+
+// Verify service_name is 'salesforce'
+if (service_name !== 'salesforce') {
+ return res.redirect(`/oauth/error?message=Invalid service name`);
+}
+```
+
+##### 2. Exchange Authorization Code for Tokens
+```javascript
+const axios = require('axios');
+
+try {
+ const tokenResponse = await axios.post(
+ 'https://login.salesforce.com/services/oauth2/token',
+ null,
+ {
+ params: {
+ grant_type: 'authorization_code',
+ code: authorization_code,
+ client_id: process.env.SALESFORCE_CLIENT_ID,
+ client_secret: process.env.SALESFORCE_CLIENT_SECRET,
+ redirect_uri: `${process.env.BACKEND_URL}/api/v1/users/oauth/callback?user_uuid=${user_uuid}&service_name=salesforce`
+ },
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+ }
+ );
+
+ const {
+ access_token,
+ refresh_token,
+ instance_url,
+ id,
+ issued_at,
+ signature
+ } = tokenResponse.data;
+
+ // Store tokens...
+
+} catch (error) {
+ console.error('Token exchange failed:', error);
+ return res.redirect(`/oauth/error?message=${encodeURIComponent(error.message)}`);
+}
+```
+
+##### 3. Store Tokens Securely
+```javascript
+// Encrypt tokens before storing
+const encryptedAccessToken = encrypt(access_token);
+const encryptedRefreshToken = encrypt(refresh_token);
+
+await db.integrations.upsert({
+ where: {
+ user_id: user_uuid,
+ service_name: 'salesforce'
+ },
+ update: {
+ access_token: encryptedAccessToken,
+ refresh_token: encryptedRefreshToken,
+ instance_url: instance_url,
+ token_issued_at: new Date(parseInt(issued_at)),
+ updated_at: new Date()
+ },
+ create: {
+ user_id: user_uuid,
+ service_name: 'salesforce',
+ access_token: encryptedAccessToken,
+ refresh_token: encryptedRefreshToken,
+ instance_url: instance_url,
+ token_issued_at: new Date(parseInt(issued_at)),
+ created_at: new Date()
+ }
+});
+```
+
+##### 4. Return Success Response
+```javascript
+// Option 1: Redirect to success page with status
+res.redirect('/oauth/success?status=success&service=salesforce');
+
+// Option 2: Return HTML page that mobile app can detect
+res.send(`
+
+
+
+ Authentication Successful
+
+
+
+
+
+
โ
+
Authentication Successful!
+
Your Salesforce account has been connected.
+
You can close this window now.
+
+
+
+
+`);
+```
+
+##### 5. Handle Errors
+```javascript
+// If token exchange or storage fails
+res.send(`
+
+
+
+ Authentication Failed
+
+
+
+
+
+
โ
+
Authentication Failed
+
${errorMessage || 'An error occurred during authentication.'}
+
Please try again.
+
+
+
+
+`);
+```
+
+---
+
+## Mobile App Detection Logic
+
+The mobile app (SalesforceAuth component) detects the backend callback using these helper functions:
+
+### Detection Functions
+```typescript
+// Check if URL is the backend callback URI
+const isBackendCallbackUri = (url: string): boolean => {
+ return url.includes('/api/v1/users/oauth/callback');
+};
+
+// Check if backend callback indicates success
+const isCallbackSuccess = (url: string): boolean => {
+ const status = getQueryParamFromUrl(url, 'status');
+ const success = getQueryParamFromUrl(url, 'success');
+ return status === 'success' || success === 'true';
+};
+
+// Check if backend callback indicates error
+const isCallbackError = (url: string): boolean => {
+ const status = getQueryParamFromUrl(url, 'status');
+ const error = getQueryParamFromUrl(url, 'error');
+ return status === 'error' || status === 'failure' || !!error;
+};
+```
+
+### Navigation State Handler
+```typescript
+handleNavigationStateChange = (navState) => {
+ const { url, loading } = navState;
+
+ // Check if this is the backend callback URL
+ if (isBackendCallbackUri(url)) {
+ console.log('Backend callback detected');
+
+ // Show processing modal
+ setState({ processing: true, processingStep: 'Processing...' });
+
+ // Check for success
+ if (isCallbackSuccess(url)) {
+ handleBackendSuccess();
+ return;
+ }
+
+ // Check for error
+ if (isCallbackError(url)) {
+ const errorMessage = getQueryParamFromUrl(url, 'message') || 'Authentication failed';
+ handleBackendError(errorMessage);
+ return;
+ }
+ }
+};
+```
+
+---
+
+## Complete Backend Example (Node.js/Express)
+
+```javascript
+const express = require('express');
+const axios = require('axios');
+const crypto = require('crypto');
+
+const router = express.Router();
+
+// Encryption utilities
+function encrypt(text) {
+ const algorithm = 'aes-256-gcm';
+ const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
+ const iv = crypto.randomBytes(16);
+
+ const cipher = crypto.createCipheriv(algorithm, key, iv);
+ let encrypted = cipher.update(text, 'utf8', 'hex');
+ encrypted += cipher.final('hex');
+
+ const authTag = cipher.getAuthTag();
+
+ return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
+}
+
+// OAuth Callback Endpoint
+router.get('/api/v1/users/oauth/callback', async (req, res) => {
+ const { code: authorization_code, user_uuid, service_name } = req.query;
+
+ console.log('[OAuth Callback] Received:', { user_uuid, service_name, hasCode: !!authorization_code });
+
+ // Validate required parameters
+ if (!authorization_code || !user_uuid || !service_name) {
+ return res.send(errorPage('Missing required parameters'));
+ }
+
+ if (service_name !== 'salesforce') {
+ return res.send(errorPage('Invalid service name'));
+ }
+
+ try {
+ // Exchange authorization code for tokens
+ const tokenResponse = await axios.post(
+ 'https://login.salesforce.com/services/oauth2/token',
+ null,
+ {
+ params: {
+ grant_type: 'authorization_code',
+ code: authorization_code,
+ client_id: process.env.SALESFORCE_CLIENT_ID,
+ client_secret: process.env.SALESFORCE_CLIENT_SECRET,
+ redirect_uri: `${process.env.BACKEND_URL}/api/v1/users/oauth/callback?user_uuid=${user_uuid}&service_name=salesforce`
+ },
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+ }
+ );
+
+ const {
+ access_token,
+ refresh_token,
+ instance_url,
+ id,
+ issued_at
+ } = tokenResponse.data;
+
+ console.log('[OAuth Callback] Token exchange successful');
+
+ // Encrypt and store tokens
+ const encryptedAccessToken = encrypt(access_token);
+ const encryptedRefreshToken = encrypt(refresh_token);
+
+ await db.integrations.upsert({
+ where: {
+ user_id: user_uuid,
+ service_name: 'salesforce'
+ },
+ update: {
+ access_token: encryptedAccessToken,
+ refresh_token: encryptedRefreshToken,
+ instance_url: instance_url,
+ token_issued_at: new Date(parseInt(issued_at)),
+ updated_at: new Date()
+ },
+ create: {
+ user_id: user_uuid,
+ service_name: 'salesforce',
+ access_token: encryptedAccessToken,
+ refresh_token: encryptedRefreshToken,
+ instance_url: instance_url,
+ token_issued_at: new Date(parseInt(issued_at)),
+ created_at: new Date()
+ }
+ });
+
+ console.log('[OAuth Callback] Tokens stored successfully');
+
+ // Return success page
+ return res.send(successPage());
+
+ } catch (error) {
+ console.error('[OAuth Callback] Error:', error);
+ return res.send(errorPage(error.message));
+ }
+});
+
+// Success page HTML
+function successPage() {
+ return `
+
+
+
+ Success
+
+
+
+
+
+
โ
+
Authentication Successful!
+
Your Salesforce account has been connected.
+
+
+
+
+ `;
+}
+
+// Error page HTML
+function errorPage(message) {
+ return `
+
+
+
+ Error
+
+
+
+
+
+
โ
+
Authentication Failed
+
${message || 'An error occurred.'}
+
+
+
+
+ `;
+}
+
+module.exports = router;
+```
+
+---
+
+## Configuration Checklist
+
+### Salesforce Connected App
+- โ
Callback URL: `https://YOUR_BACKEND_URL/api/v1/users/oauth/callback`
+- โ
OAuth Scopes: api, refresh_token, offline_access, profile, email, address, phone
+
+### Mobile App (SalesforceAuth.tsx)
+- โ
`BACKEND_BASE_URL`: Your backend domain
+- โ
`CALLBACK_PATH`: `/api/v1/users/oauth/callback`
+- โ
`CLIENT_ID`: Salesforce Consumer Key
+
+### Backend Environment Variables
+- โ
`SALESFORCE_CLIENT_ID`: Consumer Key from Salesforce
+- โ
`SALESFORCE_CLIENT_SECRET`: Consumer Secret from Salesforce
+- โ
`BACKEND_URL`: Your public backend URL
+- โ
`ENCRYPTION_KEY`: 32-byte hex string for token encryption
+
+---
+
+## Testing
+
+1. **Test OAuth Flow**:
+ - Open app โ CRM & Sales โ Salesforce
+ - Complete Salesforce login
+ - Verify redirect to backend callback
+ - Check backend logs for token exchange
+ - Verify success page appears
+ - Confirm modal closes automatically
+
+2. **Test Error Handling**:
+ - Simulate invalid credentials
+ - Simulate network failure
+ - Verify error page displays
+ - Confirm retry functionality works
+
+3. **Verify Token Storage**:
+ - Check database for encrypted tokens
+ - Verify user_id association
+ - Confirm refresh_token is stored
+
+---
+
+## Security Considerations
+
+1. **HTTPS Only**: Always use HTTPS for callback URLs
+2. **Token Encryption**: Encrypt tokens before storing in database
+3. **Client Secret**: Never expose client secret to mobile app
+4. **State Parameter**: Add CSRF protection using state parameter
+5. **Token Expiration**: Implement token refresh logic
+6. **Access Control**: Validate user_uuid belongs to authenticated session
+
+---
+
+## Troubleshooting
+
+| Issue | Solution |
+|-------|----------|
+| Callback URL mismatch | Ensure Salesforce Connected App callback URL exactly matches backend URL with query params |
+| Token exchange fails | Check client_id and client_secret are correct |
+| Authorization code expired | Codes expire in 15 minutes - ensure timely exchange |
+| Backend not reachable | Verify backend URL is publicly accessible (use ngrok for local testing) |
+| WebView doesn't detect success | Ensure backend returns HTML with `?status=success` in URL |
+
+---
+
+**Last Updated**: October 2025
+**Version**: 2.0.0 (Backend Callback Flow)
+
diff --git a/src/modules/integrations/screens/SALESFORCE_SETUP.md b/src/modules/integrations/screens/SALESFORCE_SETUP.md
new file mode 100644
index 0000000..d31bb02
--- /dev/null
+++ b/src/modules/integrations/screens/SALESFORCE_SETUP.md
@@ -0,0 +1,452 @@
+# Salesforce OAuth Integration Setup Guide
+
+This guide will help you configure Salesforce OAuth authentication for your Centralized Reporting System application.
+
+## Table of Contents
+1. [Prerequisites](#prerequisites)
+2. [Create a Salesforce Connected App](#create-a-salesforce-connected-app)
+3. [Configure OAuth Settings](#configure-oauth-settings)
+4. [Update Application Configuration](#update-application-configuration)
+5. [Test the Integration](#test-the-integration)
+6. [Troubleshooting](#troubleshooting)
+
+---
+
+## Prerequisites
+
+Before you begin, ensure you have:
+- A Salesforce account (Production or Sandbox)
+- Administrator access to Salesforce
+- Your backend API ready to handle OAuth token exchange
+- React Native app with WebView capability
+
+---
+
+## Create a Salesforce Connected App
+
+### Step 1: Access Setup
+1. Log in to your Salesforce account
+2. Click the **gear icon** (โ๏ธ) in the top right corner
+3. Select **Setup** from the dropdown menu
+
+### Step 2: Navigate to App Manager
+1. In the Quick Find box, type **"App Manager"**
+2. Click **App Manager** under Platform Tools > Apps
+3. Click **New Connected App** button
+
+### Step 3: Basic Information
+Fill in the following required fields:
+- **Connected App Name**: `Centralized Reporting System` (or your preferred name)
+- **API Name**: `Centralized_Reporting_System` (auto-populated)
+- **Contact Email**: Your email address
+
+### Step 4: API (Enable OAuth Settings)
+1. Check the box **"Enable OAuth Settings"**
+2. **Callback URL**: Enter your redirect URI
+ ```
+ centralizedreportingsystem://oauth/salesforce/callback
+ ```
+ > Note: You can add multiple callback URLs for different environments (development, staging, production)
+
+3. **Selected OAuth Scopes**: Select the following scopes and move them to "Selected OAuth Scopes":
+ - Access the identity URL service (id, profile, email, address, phone)
+ - Access unique user identifiers (openid)
+ - Perform requests at any time (refresh_token, offline_access)
+ - Manage user data via APIs (api)
+ - Full access to all data (full) - Optional, use with caution
+
+4. **Additional OAuth Settings**:
+ - โ
**Enable for Device Flow**: Check this if you want to support device authentication
+ - โ
**Require Secret for Web Server Flow**: Recommended for security
+ - โ
**Require Secret for Refresh Token Flow**: Recommended for security
+ - โ
**Enable Authorization Code and Credentials Flow**: Check this
+
+5. Click **Save** button
+
+### Step 5: Wait for Salesforce Processing
+After saving, Salesforce will process your app (usually 2-10 minutes).
+
+### Step 6: Retrieve OAuth Credentials
+1. Once processing is complete, navigate back to **App Manager**
+2. Find your Connected App and click the dropdown arrow (โผ)
+3. Select **View**
+4. In the "API (Enable OAuth Settings)" section, you'll see:
+ - **Consumer Key** (Client ID)
+ - **Consumer Secret** (Client Secret) - Click "Click to reveal"
+
+> โ ๏ธ **Important**: Keep these credentials secure! Never commit them to public repositories.
+
+---
+
+## Configure OAuth Settings
+
+### Environment-Specific Endpoints
+
+#### Production
+```
+Auth Base URL: https://login.salesforce.com
+```
+
+#### Sandbox
+```
+Auth Base URL: https://test.salesforce.com
+```
+
+### OAuth Scopes Explained
+
+| Scope | Description |
+|-------|-------------|
+| `api` | Access to all standard Salesforce REST APIs |
+| `refresh_token` | Ability to refresh the access token |
+| `offline_access` | Perform requests even when the user is offline |
+| `id` | Access to the unique identifier for the user |
+| `profile` | Access to user's profile information |
+| `email` | Access to user's email address |
+| `address` | Access to user's address |
+| `phone` | Access to user's phone number |
+| `openid` | OpenID Connect authentication |
+
+---
+
+## Update Application Configuration
+
+### Step 1: Update SalesforceAuth.tsx
+
+Open `src/modules/integrations/screens/SalesforceAuth.tsx` and update the following:
+
+```typescript
+const SALESFORCE_CONFIG = {
+ // Replace with your Consumer Key (Client ID) from Salesforce
+ CLIENT_ID: '3MVG9XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
+
+ // This must match exactly what you configured in Salesforce Connected App
+ REDIRECT_URI: 'centralizedreportingsystem://oauth/salesforce/callback',
+
+ // Use login.salesforce.com for production
+ // Use test.salesforce.com for sandbox
+ AUTH_BASE_URL: 'https://login.salesforce.com',
+
+ RESPONSE_TYPE: 'code',
+ DISPLAY: 'touch', // Optimized for mobile
+};
+```
+
+### Step 2: Configure Deep Linking (Android)
+
+#### Android - Update AndroidManifest.xml
+Add the intent filter to handle the OAuth callback:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Step 3: Configure Deep Linking (iOS)
+
+#### iOS - Update Info.plist
+Add URL types for the OAuth callback:
+
+```xml
+
+CFBundleURLTypes
+
+
+ CFBundleURLSchemes
+
+ centralizedreportingsystem
+
+ CFBundleURLName
+ com.yourcompany.centralizedreportingsystem
+
+
+```
+
+### Step 4: Backend Token Exchange Endpoint
+
+Your backend should have an endpoint to exchange the authorization code for an access token:
+
+```typescript
+// Example backend endpoint
+POST /api/v1/integrations/manage-token
+
+Request Body:
+{
+ "authorization_code": "aPrxXXXXXXXXXXXXXXXXXXXXXXXXX",
+ "id": "user-uuid",
+ "service_name": "salesforce",
+ "access_token": "user-session-token"
+}
+```
+
+The backend should:
+1. Receive the authorization code
+2. Exchange it with Salesforce for an access token
+3. Store the tokens securely (encrypted)
+4. Return success/failure response
+
+#### Salesforce Token Exchange Request
+
+Your backend should make this request to Salesforce:
+
+```http
+POST https://login.salesforce.com/services/oauth2/token
+Content-Type: application/x-www-form-urlencoded
+
+grant_type=authorization_code
+&code={AUTHORIZATION_CODE}
+&client_id={YOUR_CLIENT_ID}
+&client_secret={YOUR_CLIENT_SECRET}
+&redirect_uri={YOUR_REDIRECT_URI}
+```
+
+#### Expected Salesforce Response
+
+```json
+{
+ "access_token": "00D...ABC",
+ "refresh_token": "5Aep...XYZ",
+ "signature": "signature-value",
+ "scope": "api refresh_token offline_access id profile email",
+ "id_token": "eyJ...",
+ "instance_url": "https://yourinstance.salesforce.com",
+ "id": "https://login.salesforce.com/id/00D.../005...",
+ "token_type": "Bearer",
+ "issued_at": "1699999999999"
+}
+```
+
+---
+
+## Test the Integration
+
+### Testing Steps
+
+1. **Launch the App**
+ - Open your React Native application
+ - Navigate to **Integrations** > **CRM & Sales Integration**
+
+2. **Select Salesforce**
+ - Tap on the **Salesforce** option
+ - The Salesforce authentication WebView should open
+
+3. **Authenticate**
+ - Enter your Salesforce username and password
+ - If using Two-Factor Authentication, complete the verification
+ - Review and approve the OAuth permissions
+
+4. **Verify Success**
+ - The app should capture the authorization code
+ - Backend should exchange it for tokens
+ - You should see a success message
+ - The auth modal should close automatically
+
+5. **Check Data Sync** (if implemented)
+ - Navigate to the Salesforce dashboard
+ - Verify that data is being synced properly
+
+### Testing with Different Salesforce Orgs
+
+To test with different Salesforce organizations:
+1. Tap the **Re-auth** button next to Salesforce
+2. Confirm the re-authentication
+3. Log in with a different Salesforce account
+
+---
+
+## Troubleshooting
+
+### Common Issues and Solutions
+
+#### 1. **"redirect_uri_mismatch" Error**
+
+**Problem**: The redirect URI doesn't match what's configured in Salesforce.
+
+**Solution**:
+- Ensure the redirect URI in `SalesforceAuth.tsx` exactly matches what's in the Salesforce Connected App
+- Check for trailing slashes, capitalization, and special characters
+- Both values must be identical: `centralizedreportingsystem://oauth/salesforce/callback`
+
+---
+
+#### 2. **"invalid_client_id" Error**
+
+**Problem**: The Client ID is incorrect or the Connected App is not yet active.
+
+**Solution**:
+- Wait 2-10 minutes after creating the Connected App for Salesforce to process it
+- Double-check the Consumer Key (Client ID) from Salesforce matches your config
+- Ensure there are no extra spaces or characters
+
+---
+
+#### 3. **WebView Doesn't Load**
+
+**Problem**: The Salesforce login page doesn't appear.
+
+**Solution**:
+- Check your internet connection
+- Verify the `AUTH_BASE_URL` is correct (login.salesforce.com or test.salesforce.com)
+- Check console logs for error messages
+- Ensure WebView has internet permission (Android)
+
+---
+
+#### 4. **App Doesn't Capture the Code**
+
+**Problem**: After authentication, the app doesn't proceed.
+
+**Solution**:
+- Verify deep linking is configured correctly for both iOS and Android
+- Check that the redirect URI scheme matches your app's URL scheme
+- Test the deep link manually using ADB (Android) or xcrun (iOS)
+- Check console logs for navigation state changes
+
+---
+
+#### 5. **"access_denied" Error**
+
+**Problem**: User denied the OAuth permissions.
+
+**Solution**:
+- User must approve all requested permissions
+- If permissions are too broad, consider reducing the requested scopes
+- Check if the Connected App is enabled for the user's profile
+
+---
+
+#### 6. **Token Exchange Fails**
+
+**Problem**: Backend returns an error when exchanging the authorization code.
+
+**Solution**:
+- Verify backend endpoint is working: `/api/v1/integrations/manage-token`
+- Check that the backend has the correct Consumer Secret
+- Authorization codes expire after 15 minutes - ensure timely exchange
+- Check backend logs for detailed error messages
+- Verify the authorization code is being sent correctly
+
+---
+
+#### 7. **Sandbox vs Production Confusion**
+
+**Problem**: Using the wrong Salesforce environment.
+
+**Solution**:
+- **Production**: Use `https://login.salesforce.com`
+- **Sandbox**: Use `https://test.salesforce.com`
+- Create separate Connected Apps for each environment
+- Update the config based on your current environment
+
+---
+
+### Debug Logging
+
+Enable comprehensive logging to troubleshoot issues:
+
+```typescript
+// In SalesforceAuth.tsx, check console logs for:
+console.log('[SalesforceAuth] Authorization code received:', authCode);
+console.log('[SalesforceAuth] Response from manageToken:', response);
+console.log('[SalesforceAuth] nav change url:', url, 'loading:', loading);
+```
+
+---
+
+## Security Best Practices
+
+1. **Never Store Credentials in Code**
+ - Use environment variables or secure config management
+ - Never commit `.env` files with credentials to version control
+
+2. **Use HTTPS Only**
+ - Always use `https://` for Salesforce endpoints
+ - Never use `http://` in production
+
+3. **Implement Token Refresh**
+ - Store refresh tokens securely
+ - Implement automatic token refresh on expiration
+ - Handle refresh token failures gracefully
+
+4. **Encrypt Stored Tokens**
+ - Use encryption for tokens stored on device
+ - Use secure storage mechanisms (Keychain, KeyStore)
+
+5. **Validate State Parameter**
+ - Include CSRF protection using state parameter
+ - Verify state on callback to prevent CSRF attacks
+
+6. **Limit OAuth Scopes**
+ - Request only the minimum scopes needed
+ - Avoid using `full` scope unless absolutely necessary
+
+7. **Monitor Token Usage**
+ - Log token usage and refresh patterns
+ - Set up alerts for unusual authentication patterns
+
+---
+
+## Additional Resources
+
+### Salesforce Documentation
+- [Connected Apps Overview](https://help.salesforce.com/s/articleView?id=sf.connected_app_overview.htm)
+- [OAuth 2.0 Authorization Flow](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_flow.htm)
+- [OAuth Scopes](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_tokens_scopes.htm)
+
+### React Native Resources
+- [React Native WebView](https://github.com/react-native-webview/react-native-webview)
+- [Deep Linking in React Native](https://reactnative.dev/docs/linking)
+
+---
+
+## Support
+
+If you encounter issues not covered in this guide:
+
+1. Check Salesforce Setup Audit Trail for any configuration errors
+2. Review React Native console logs
+3. Check backend API logs for token exchange errors
+4. Verify network requests in the debugger
+5. Test with both Production and Sandbox environments
+
+---
+
+## Changelog
+
+### Version 1.0.0
+- Initial Salesforce OAuth integration
+- Support for mobile OAuth flow
+- Backend token exchange implementation
+- Data sync scheduling
+
+---
+
+## License
+
+This integration follows your application's license terms.
+
+---
+
+**Last Updated**: October 2025
+**Maintained By**: Your Development Team
+
diff --git a/src/modules/integrations/screens/SalesforceAuth.tsx b/src/modules/integrations/screens/SalesforceAuth.tsx
new file mode 100644
index 0000000..7278736
--- /dev/null
+++ b/src/modules/integrations/screens/SalesforceAuth.tsx
@@ -0,0 +1,689 @@
+import React, { useState, useRef, useCallback, useMemo } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ Alert,
+ ActivityIndicator,
+ TouchableOpacity,
+ SafeAreaView,
+} from 'react-native';
+import { WebView } from 'react-native-webview';
+import Icon from 'react-native-vector-icons/MaterialIcons';
+import { useTheme } from '@/shared/styles/useTheme';
+import { useNavigation } from '@react-navigation/native';
+import { manageToken } from '../services/integrationAPI';
+import { useSelector } from 'react-redux';
+import { RootState } from '@/store/store';
+import http from '@/services/http';
+
+// Types
+interface SalesforceAuthProps {
+ onAuthSuccess?: (authData: SalesforceAuthData) => void;
+ onAuthError?: (error: string) => void;
+ onClose?: () => void;
+}
+
+interface SalesforceAuthData {
+ accessToken: string;
+ refreshToken?: string;
+ instanceUrl?: string;
+ id?: string;
+ issuedAt?: string;
+ signature?: string;
+ tokenType?: string;
+}
+
+interface SalesforceAuthState {
+ loading: boolean;
+ error: string | null;
+ currentUrl: string;
+ processing: boolean;
+ processingStep: string;
+}
+
+// Salesforce OAuth Configuration
+const SALESFORCE_CONFIG = {
+ // Replace with your actual Salesforce Connected App credentials
+ CLIENT_ID: '3MVG9GBhY6wQjl2sueQtv2NXMm3EuWtEvOQoeKRAzYcgs2.AWhkCPFitVFPYyUkiLRRdIww2fpr48_Inokd3F',
+ // Backend callback URL that will handle the OAuth callback
+ BACKEND_BASE_URL: 'https://512acb53a4a4.ngrok-free.app',
+ CALLBACK_PATH: '/api/v1/users/oauth/callback',
+ // Use login.salesforce.com for production, test.salesforce.com for sandbox
+ AUTH_BASE_URL: 'https://test.salesforce.com',
+ RESPONSE_TYPE: 'code',
+ DISPLAY: 'touch', // Optimized for mobile
+};
+
+// Salesforce OAuth scopes - request access to various Salesforce APIs
+const SALESFORCE_SCOPES = [
+ 'api', // Access to REST API
+ 'refresh_token', // Ability to use refresh token
+ 'offline_access', // Perform requests at any time
+ // 'id', // Access to unique identifier
+ 'profile', // Access to profile information
+ 'email', // Access to email address
+ 'address', // Access to address information
+ 'phone', // Access to phone number
+ // 'openid', // OpenID Connect
+].join(' ');
+
+// Build Salesforce OAuth URL with user-specific callback
+const buildSalesforceAuthUrl = (userUuid: string): string => {
+ const baseUrl = `${SALESFORCE_CONFIG.AUTH_BASE_URL}/services/oauth2/authorize`;
+
+ // Build the redirect URI with query parameters that backend expects
+ const redirectUri = `${SALESFORCE_CONFIG.BACKEND_BASE_URL}${SALESFORCE_CONFIG.CALLBACK_PATH}?user_uuid=${encodeURIComponent(userUuid)}&service_name=salesforce`;
+
+ const params = new URLSearchParams({
+ client_id: SALESFORCE_CONFIG.CLIENT_ID,
+ redirect_uri: redirectUri,
+ response_type: SALESFORCE_CONFIG.RESPONSE_TYPE,
+ display: SALESFORCE_CONFIG.DISPLAY,
+ scope: SALESFORCE_SCOPES,
+ // Add state parameter for security (CSRF protection)
+ state: Math.random().toString(36).substring(7),
+ });
+
+ console.log('[SalesforceAuth] Built OAuth URL:', `${baseUrl}?${params.toString()}`);
+ console.log('[SalesforceAuth] Redirect URI:', redirectUri);
+ return `${baseUrl}?${params.toString()}`;
+};
+
+// Safe query param parser for React Native
+const getQueryParamFromUrl = (url: string, key: string): string | null => {
+ try {
+ const queryIndex = url.indexOf('?');
+ if (queryIndex === -1) return null;
+ const query = url.substring(queryIndex + 1);
+ const pairs = query.split('&');
+ for (let i = 0; i < pairs.length; i += 1) {
+ const [rawK, rawV] = pairs[i].split('=');
+ const k = decodeURIComponent(rawK || '');
+ if (k === key) {
+ return decodeURIComponent(rawV || '');
+ }
+ }
+ return null;
+ } catch (e) {
+ return null;
+ }
+};
+
+// Parse all query params from URL into a plain object
+const getAllQueryParamsFromUrl = (url: string): Record => {
+ const result: Record = {};
+ try {
+ const queryIndex = url.indexOf('?');
+ if (queryIndex === -1) return result;
+ const query = url.substring(queryIndex + 1);
+ const pairs = query.split('&');
+ for (let i = 0; i < pairs.length; i += 1) {
+ const [rawK, rawV] = pairs[i].split('=');
+ const k = decodeURIComponent(rawK || '');
+ const v = decodeURIComponent(rawV || '');
+ if (!k) continue;
+ // Coerce numeric values when appropriate
+ const numeric = /^-?\d+$/.test(v) ? Number(v) : v;
+ result[k] = numeric;
+ }
+ } catch (e) {
+ // swallow errors, return best-effort result
+ }
+ return result;
+};
+
+// Extract authorization code from URL
+const extractAuthCode = (url: string): string | null => {
+ return getQueryParamFromUrl(url, 'code');
+};
+
+// Check if URL is the backend callback URI
+const isBackendCallbackUri = (url: string): boolean => {
+ return url.includes(SALESFORCE_CONFIG.CALLBACK_PATH);
+};
+
+// Check if backend callback indicates success
+const isCallbackSuccess = (url: string): boolean => {
+ const status = getQueryParamFromUrl(url, 'status');
+ const success = getQueryParamFromUrl(url, 'success');
+ return status === 'success' || success === 'true';
+};
+
+// Check if backend callback indicates error
+const isCallbackError = (url: string): boolean => {
+ const status = getQueryParamFromUrl(url, 'status');
+ const error = getQueryParamFromUrl(url, 'error');
+ return status === 'error' || status === 'failure' || !!error;
+};
+
+const SalesforceAuth: React.FC = ({
+ onAuthSuccess,
+ onAuthError,
+ onClose,
+}) => {
+ const { colors, spacing, fonts, shadows } = useTheme();
+ const webViewRef = useRef(null);
+ const navigation = useNavigation();
+ const { user, accessToken } = useSelector((state: RootState) => state.auth);
+
+ const [state, setState] = useState({
+ loading: true,
+ error: null,
+ currentUrl: buildSalesforceAuthUrl(user?.uuid || ''),
+ processing: false,
+ processingStep: '',
+ });
+
+ // Handle authorization code from callback URL
+ const handleAuthorizationCode = useCallback(async (authCode: string) => {
+ console.log('[SalesforceAuth] Authorization code captured:', authCode);
+ console.log('[SalesforceAuth] Manually hitting callback URL with query params');
+
+ // Show processing modal
+ setState(prev => ({
+ ...prev,
+ processing: true,
+ processingStep: 'Connecting to Salesforce and storing credentials...'
+ }));
+
+ try {
+ // Manually hit the callback URL endpoint with query parameters
+ // This mimics what Salesforce would have sent directly to the backend
+ const response = await http.get('/api/v1/users/oauth/callback', {
+ authorization_code: authCode,
+ user_uuid: user?.uuid || '',
+ service_name: 'salesforce'
+ });
+
+ console.log('[SalesforceAuth] Response from callback endpoint:', response);
+
+ // Check if response status is success
+ if (response?.data && typeof response.data === 'object' && 'status' in response.data && response.data.status === 'success') {
+ console.log('[SalesforceAuth] Token exchange successful');
+
+ // Update processing step
+ setState(prev => ({
+ ...prev,
+ processingStep: 'Salesforce authentication successful!'
+ }));
+
+ // Wait a moment to show success message
+ await new Promise(resolve => setTimeout(resolve, 1500));
+
+ // Hide processing modal and call success callback
+ setState(prev => ({ ...prev, processing: false, processingStep: '' }));
+
+ // Call success callback
+ onAuthSuccess?.({
+ accessToken: authCode,
+ tokenType: 'authorization_code',
+ });
+ } else {
+ const errorMessage = response?.data && typeof response.data === 'object' && 'message' in response.data
+ ? String(response.data.message)
+ : 'Failed to authenticate with Salesforce';
+ console.warn('[SalesforceAuth] Token exchange failed:', errorMessage);
+ throw new Error(errorMessage);
+ }
+ } catch (error) {
+ console.error('[SalesforceAuth] Error during token exchange:', error);
+
+ // Hide processing modal
+ setState(prev => ({ ...prev, processing: false, processingStep: '' }));
+
+ // Call error callback
+ const errorMessage = error instanceof Error ? error.message : 'Authentication failed';
+ onAuthError?.(errorMessage);
+
+ // Show error alert
+ Alert.alert(
+ 'Authentication Error',
+ errorMessage,
+ [
+ {
+ text: 'Retry',
+ onPress: () => {
+ setState(prev => ({
+ ...prev,
+ error: null,
+ currentUrl: buildSalesforceAuthUrl(user?.uuid || '')
+ }));
+ webViewRef.current?.reload();
+ },
+ },
+ {
+ text: 'Cancel',
+ onPress: onClose,
+ style: 'cancel',
+ },
+ ]
+ );
+ }
+ }, [onAuthSuccess, onAuthError, onClose, user?.uuid]);
+
+ // Handle backend callback error
+ const handleBackendError = useCallback((url: string) => {
+ const errorMessage = getQueryParamFromUrl(url, 'message') ||
+ getQueryParamFromUrl(url, 'error_description') ||
+ 'Failed to authenticate with Salesforce';
+
+ console.error('[SalesforceAuth] Backend callback error:', errorMessage);
+
+ // Hide processing modal
+ setState(prev => ({
+ ...prev,
+ processing: false,
+ processingStep: '',
+ error: errorMessage
+ }));
+
+ // Call error callback
+ onAuthError?.(errorMessage);
+
+ // Show error alert
+ Alert.alert(
+ 'Authentication Error',
+ errorMessage,
+ [
+ {
+ text: 'Retry',
+ onPress: () => {
+ setState(prev => ({
+ ...prev,
+ error: null,
+ currentUrl: buildSalesforceAuthUrl(user?.uuid || '')
+ }));
+ webViewRef.current?.reload();
+ },
+ },
+ {
+ text: 'Cancel',
+ onPress: onClose,
+ style: 'cancel',
+ },
+ ]
+ );
+ }, [onAuthError, onClose, user?.uuid]);
+
+ // Handle WebView navigation state changes
+ const handleNavigationStateChange = useCallback((navState: any) => {
+ const { url, loading } = navState;
+ console.log('[SalesforceAuth] nav change url:', url, 'loading:', loading);
+
+ setState(prev => ({
+ ...prev,
+ loading,
+ currentUrl: url,
+ }));
+
+ // Check if this is the backend callback URL (after Salesforce redirects)
+ if (isBackendCallbackUri(url)) {
+ console.log('[SalesforceAuth] Backend callback detected');
+
+ // Extract authorization code from the URL
+ const authCode = extractAuthCode(url);
+ console.log('[SalesforceAuth] Authorization code in URL:', authCode);
+
+ // If we have an authorization code, capture it and send to backend
+ if (authCode && !state.processing) {
+ console.log('[SalesforceAuth] Capturing authorization code and sending to backend');
+ handleAuthorizationCode(authCode);
+ return;
+ }
+
+ // Check for success or error status (if backend already processed and redirected)
+ if (isCallbackSuccess(url)) {
+ console.log('[SalesforceAuth] Backend callback indicates success (already processed)');
+ // Just close the modal - backend already handled everything
+ setState(prev => ({ ...prev, processing: false }));
+ onAuthSuccess?.({
+ accessToken: 'token_exchanged_by_backend',
+ tokenType: 'Bearer',
+ });
+ return;
+ }
+
+ if (isCallbackError(url)) {
+ console.log('[SalesforceAuth] Backend callback indicates error');
+ handleBackendError(url);
+ return;
+ }
+
+ // If no code and no status, show processing
+ if (!state.processing) {
+ setState(prev => ({
+ ...prev,
+ processing: true,
+ processingStep: 'Processing Salesforce authentication...'
+ }));
+ }
+ return;
+ }
+
+ // Handle Salesforce OAuth errors (before redirect to backend)
+ const oauthError = getQueryParamFromUrl(url, 'error');
+ if (oauthError) {
+ const errorDescription = getQueryParamFromUrl(url, 'error_description') || oauthError;
+ console.error('[SalesforceAuth] OAuth error:', errorDescription);
+ handleAuthError(errorDescription);
+ return;
+ }
+ }, [state.processing, handleAuthorizationCode, handleBackendError, onAuthSuccess]);
+
+ // Allow WebView to load all URLs (backend callback is HTTP, not custom scheme)
+ const handleShouldStartLoadWithRequest = useCallback((request: any) => {
+ const { url } = request || {};
+ if (!url) return true;
+
+ // Log for debugging
+ console.log('[SalesforceAuth] onShouldStart load:', url);
+
+ // Allow all HTTP/HTTPS URLs (backend callback will be handled in handleNavigationStateChange)
+ return true;
+ }, []);
+
+ // Handle authentication error
+ const handleAuthError = useCallback((error: string) => {
+ setState(prev => ({ ...prev, error, loading: false }));
+ onAuthError?.(error);
+
+ Alert.alert(
+ 'Salesforce Authentication Error',
+ error,
+ [
+ {
+ text: 'Retry',
+ onPress: () => {
+ setState(prev => ({
+ ...prev,
+ error: null,
+ currentUrl: buildSalesforceAuthUrl(user?.uuid || '')
+ }));
+ webViewRef.current?.reload();
+ },
+ },
+ {
+ text: 'Cancel',
+ onPress: onClose,
+ style: 'cancel',
+ },
+ ]
+ );
+ }, [onAuthError, onClose, user?.uuid]);
+
+ // Handle WebView error
+ const handleWebViewError = useCallback((syntheticEvent: any) => {
+ const { nativeEvent } = syntheticEvent;
+ console.error('WebView error:', nativeEvent);
+ handleAuthError('Failed to load Salesforce login page. Please check your internet connection.');
+ }, [handleAuthError]);
+
+ const handleLoadStart = useCallback((e: any) => {
+ console.log('[SalesforceAuth] load start:', e?.nativeEvent?.url);
+ }, []);
+
+ const handleLoadEnd = useCallback((e: any) => {
+ console.log('[SalesforceAuth] load end:', e?.nativeEvent?.url);
+ }, []);
+
+ // Handle close button press
+ const handleClose = useCallback(() => {
+ onClose?.();
+ }, [onClose]);
+
+ // Handle reload
+ const handleReload = useCallback(() => {
+ setState(prev => ({
+ ...prev,
+ error: null,
+ currentUrl: buildSalesforceAuthUrl(user?.uuid || '')
+ }));
+ webViewRef.current?.reload();
+ }, [user?.uuid]);
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Salesforce Authentication
+
+
+
+
+
+
+
+
+ {/* Error State */}
+ {state.error && (
+
+
+
+ Authentication Failed
+
+
+ {state.error}
+
+
+
+ Try Again
+
+
+
+ )}
+
+ {/* Processing Modal */}
+ {state.processing && (
+
+
+
+
+
+
+ Setting Up Salesforce
+
+
+ {state.processingStep}
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Loading Overlay */}
+ {state.loading && !state.error && (
+
+
+
+ Loading Salesforce Login...
+
+
+ )}
+
+ {/* WebView */}
+ {!state.error && !state.processing && (
+ (
+
+
+
+ Loading...
+
+
+ )}
+ />
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: '#E2E8F0',
+ },
+ headerContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ headerTitle: {
+ fontSize: 18,
+ marginLeft: 8,
+ },
+ closeButton: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ errorContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ },
+ errorTitle: {
+ fontSize: 20,
+ marginTop: 16,
+ marginBottom: 8,
+ },
+ errorMessage: {
+ fontSize: 14,
+ textAlign: 'center',
+ lineHeight: 20,
+ marginBottom: 24,
+ },
+ retryButton: {
+ paddingHorizontal: 24,
+ paddingVertical: 12,
+ borderRadius: 8,
+ },
+ retryButtonText: {
+ fontSize: 16,
+ },
+ loadingOverlay: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ justifyContent: 'center',
+ alignItems: 'center',
+ zIndex: 1000,
+ },
+ loadingText: {
+ fontSize: 16,
+ marginTop: 16,
+ },
+ webView: {
+ flex: 1,
+ },
+ webViewLoading: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ webViewLoadingText: {
+ fontSize: 14,
+ marginTop: 8,
+ },
+ processingOverlay: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ justifyContent: 'center',
+ alignItems: 'center',
+ zIndex: 2000,
+ },
+ processingModal: {
+ borderRadius: 16,
+ padding: 32,
+ alignItems: 'center',
+ minWidth: 280,
+ maxWidth: 320,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.2,
+ shadowRadius: 8,
+ elevation: 8,
+ },
+ processingIconContainer: {
+ marginBottom: 20,
+ },
+ processingTitle: {
+ fontSize: 20,
+ marginBottom: 12,
+ textAlign: 'center',
+ },
+ processingStep: {
+ fontSize: 14,
+ textAlign: 'center',
+ lineHeight: 20,
+ marginBottom: 20,
+ },
+ processingDots: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ dot: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ marginHorizontal: 4,
+ },
+});
+
+export default SalesforceAuth;
+
diff --git a/src/services/http.ts b/src/services/http.ts
index ffe7536..b5e0d0d 100644
--- a/src/services/http.ts
+++ b/src/services/http.ts
@@ -8,9 +8,9 @@ import { clearSelectedService } from '@/modules/integrations/store/integrationsS
let pendingRequest: any = null;
const http = create({
- // baseURL: 'http://192.168.1.20:4000',
- baseURL: 'http://160.187.167.216',
- // baseURL: 'https://angry-gifts-shave.loca.lt',
+ // baseURL: 'http://192.168.1.17:4000',
+ // baseURL: 'http://160.187.167.216',
+ baseURL: 'https://512acb53a4a4.ngrok-free.app',
timeout: 10000,
});
diff --git a/src/store/store.ts b/src/store/store.ts
index e6538b2..ac3320d 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -10,6 +10,7 @@ import zohoProjectsSlice from '@/modules/zohoProjects/store/zohoProjectsSlice';
import profileSlice from '@/modules/profile/store/profileSlice';
import integrationsSlice from '@/modules/integrations/store/integrationsSlice';
import crmSlice from '@/modules/crm/zoho/store/crmSlice';
+import salesforceCrmSlice from '@/modules/crm/salesforce/store/salesforceCrmSlice';
import zohoBooksSlice from '@/modules/finance/zoho/store/zohoBooksSlice';
const rootReducer = combineReducers({
@@ -19,6 +20,7 @@ const rootReducer = combineReducers({
profile: profileSlice.reducer,
integrations: integrationsSlice.reducer,
crm: crmSlice,
+ salesforceCrm: salesforceCrmSlice,
zohoBooks: zohoBooksSlice,
ui: uiSlice.reducer,
});
@@ -26,7 +28,7 @@ const rootReducer = combineReducers({
const persistConfig = {
key: 'root',
storage: AsyncStorage,
- whitelist: ['auth', 'zohoPeople', 'zohoProjects', 'profile', 'integrations', 'crm', 'zohoBooks'],
+ whitelist: ['auth', 'zohoPeople', 'zohoProjects', 'profile', 'integrations', 'crm', 'salesforceCrm', 'zohoBooks'],
blacklist: ['ui'],
};