sales force integrated

This commit is contained in:
yashwin-foxy 2025-10-10 12:05:17 +05:30
parent 536a72ff4a
commit af8cc5d52d
19 changed files with 5232 additions and 16 deletions

View File

@ -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
<!DOCTYPE html>
<html>
<head><title>Success</title></head>
<body>
<h1>✓ Authentication Successful!</h1>
<p>Your Salesforce account has been connected.</p>
<script>
// Mobile app will detect this URL
window.location.search = '?status=success';
</script>
</body>
</html>
```
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

View File

@ -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.

View File

@ -56,5 +56,16 @@
<string>Roboto-SemiBold.ttf</string>
<string>Roboto-Thin.ttf</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>centralizedreportingsystem</string>
</array>
<key>CFBundleURLName</key>
<string>com.centralizedreportingsystem</string>
</dict>
</array>
</dict>
</plist>

View File

@ -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 = () => (
<Stack.Navigator>
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 (
<Stack.Navigator initialRouteName={initialRouteName}>
<Stack.Screen name="CrmDashboard" component={CrmDashboardScreen} options={{headerShown:false}} />
<Stack.Screen name="ZohoCrmData" component={ZohoCrmDataScreen} options={{headerShown:false}} />
<Stack.Screen name="SalesforceDashboard" component={SalesforceCrmDashboardScreen} options={{headerShown:false}} />
<Stack.Screen name="SalesforceData" component={SalesforceDataScreen} options={{headerShown:false}} />
</Stack.Navigator>
);
};
export default CrmNavigator;

View File

@ -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
<Stack.Screen
name="SalesforceDashboard"
component={SalesforceCrmDashboardScreen}
options={{headerShown:false}}
/>
<Stack.Screen
name="SalesforceData"
component={SalesforceDataScreen}
options={{headerShown:false}}
/>
```
## 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

View File

@ -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<EventCardProps> = ({ event, onPress }) => {
const { colors, fonts, shadows } = useTheme();
return (
<TouchableOpacity
style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border, ...shadows.medium }]}
onPress={onPress}
activeOpacity={0.8}
>
<View style={styles.cardHeader}>
<View style={styles.cardTitleRow}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]} numberOfLines={2}>
{event.Subject}
</Text>
{event.IsAllDayEvent && (
<View style={[styles.statusBadge, { backgroundColor: colors.primary }]}>
<Text style={[styles.statusText, { color: colors.surface, fontFamily: fonts.medium }]}>
All Day
</Text>
</View>
)}
</View>
{event.Location && (
<Text style={[styles.cardSubtitle, { color: colors.textLight, fontFamily: fonts.regular }]} numberOfLines={1}>
{event.Location}
</Text>
)}
</View>
<View style={styles.cardContent}>
{event.StartDateTime && (
<View style={styles.infoRow}>
<Icon name="calendar-start" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
Start: {new Date(event.StartDateTime).toLocaleString()}
</Text>
</View>
)}
{event.EndDateTime && (
<View style={styles.infoRow}>
<Icon name="calendar-end" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
End: {new Date(event.EndDateTime).toLocaleString()}
</Text>
</View>
)}
{event.Description && (
<View style={styles.infoRow}>
<Icon name="text" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]} numberOfLines={2}>
{event.Description}
</Text>
</View>
)}
</View>
<View style={styles.cardFooter}>
<Text style={[styles.dateText, { color: colors.textLight, fontFamily: fonts.regular }]}>
Created: {new Date(event.CreatedDate).toLocaleDateString()}
</Text>
</View>
</TouchableOpacity>
);
};
export const LeadCard: React.FC<LeadCardProps> = ({ lead, onPress }) => {
const { colors, fonts, shadows } = useTheme();
return (
<TouchableOpacity
style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border, ...shadows.medium }]}
onPress={onPress}
activeOpacity={0.8}
>
<View style={styles.cardHeader}>
<View style={styles.cardTitleRow}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]} numberOfLines={1}>
{lead.FirstName} {lead.LastName}
</Text>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(lead.Status, colors) }]}>
<Text style={[styles.statusText, { color: colors.surface, fontFamily: fonts.medium }]}>
{lead.Status}
</Text>
</View>
</View>
<Text style={[styles.cardSubtitle, { color: colors.textLight, fontFamily: fonts.regular }]} numberOfLines={1}>
{lead.Company}
</Text>
</View>
<View style={styles.cardContent}>
<View style={styles.infoRow}>
<Icon name="email-outline" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]} numberOfLines={1}>
{lead.Email}
</Text>
</View>
{lead.Phone && (
<View style={styles.infoRow}>
<Icon name="phone-outline" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
{lead.Phone}
</Text>
</View>
)}
{lead.Title && (
<View style={styles.infoRow}>
<Icon name="briefcase-outline" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
{lead.Title}
</Text>
</View>
)}
{lead.LeadSource && (
<View style={styles.infoRow}>
<Icon name="source-branch" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
Source: {lead.LeadSource}
</Text>
</View>
)}
</View>
<View style={styles.cardFooter}>
<Text style={[styles.dateText, { color: colors.textLight, fontFamily: fonts.regular }]}>
Created: {lead.CreatedDate ? new Date(lead.CreatedDate).toLocaleDateString() : 'N/A'}
</Text>
</View>
</TouchableOpacity>
);
};
export const TaskCard: React.FC<TaskCardProps> = ({ task, onPress }) => {
const { colors, fonts, shadows } = useTheme();
return (
<TouchableOpacity
style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border, ...shadows.medium }]}
onPress={onPress}
activeOpacity={0.8}
>
<View style={styles.cardHeader}>
<View style={styles.cardTitleRow}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]} numberOfLines={2}>
{task.Subject}
</Text>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(task.Status, colors) }]}>
<Text style={[styles.statusText, { color: colors.surface, fontFamily: fonts.medium }]}>
{task.Status}
</Text>
</View>
</View>
</View>
<View style={styles.cardContent}>
{task.Description && (
<Text style={[styles.descriptionText, { color: colors.text, fontFamily: fonts.regular }]} numberOfLines={2}>
{task.Description}
</Text>
)}
<View style={styles.infoRow}>
<Icon name="flag-outline" size={16} color={getPriorityColor(task.Priority)} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
Priority: {task.Priority}
</Text>
</View>
{task.ActivityDate && (
<View style={styles.infoRow}>
<Icon name="calendar-outline" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
Due: {new Date(task.ActivityDate).toLocaleDateString()}
</Text>
</View>
)}
</View>
<View style={styles.cardFooter}>
<Text style={[styles.dateText, { color: colors.textLight, fontFamily: fonts.regular }]}>
Created: {new Date(task.CreatedDate).toLocaleDateString()}
</Text>
</View>
</TouchableOpacity>
);
};
export const AccountCard: React.FC<AccountCardProps> = ({ account, onPress }) => {
const { colors, fonts, shadows } = useTheme();
return (
<TouchableOpacity
style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border, ...shadows.medium }]}
onPress={onPress}
activeOpacity={0.8}
>
<View style={styles.cardHeader}>
<View style={styles.cardTitleRow}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]} numberOfLines={1}>
{account.Name}
</Text>
{account.Type && (
<View style={[styles.statusBadge, { backgroundColor: colors.primary }]}>
<Text style={[styles.statusText, { color: colors.surface, fontFamily: fonts.medium }]}>
{account.Type}
</Text>
</View>
)}
</View>
{account.Industry && (
<Text style={[styles.cardSubtitle, { color: colors.textLight, fontFamily: fonts.regular }]} numberOfLines={1}>
{account.Industry}
</Text>
)}
</View>
<View style={styles.cardContent}>
{account.Phone && (
<View style={styles.infoRow}>
<Icon name="phone-outline" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
{account.Phone}
</Text>
</View>
)}
{account.Website && (
<View style={styles.infoRow}>
<Icon name="web" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]} numberOfLines={1}>
{account.Website}
</Text>
</View>
)}
{(account.BillingCity || account.BillingState) && (
<View style={styles.infoRow}>
<Icon name="map-marker-outline" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
{[account.BillingCity, account.BillingState, account.BillingCountry].filter(Boolean).join(', ')}
</Text>
</View>
)}
{account.AnnualRevenue && (
<View style={styles.infoRow}>
<Icon name="currency-usd" size={16} color={colors.primary} />
<Text style={[styles.infoText, { color: colors.primary, fontFamily: fonts.bold }]}>
Revenue: ${account.AnnualRevenue.toLocaleString()}
</Text>
</View>
)}
</View>
<View style={styles.cardFooter}>
<Text style={[styles.dateText, { color: colors.textLight, fontFamily: fonts.regular }]}>
Created: {new Date(account.CreatedDate).toLocaleDateString()}
</Text>
</View>
</TouchableOpacity>
);
};
export const OpportunityCard: React.FC<OpportunityCardProps> = ({ opportunity, onPress }) => {
const { colors, fonts, shadows } = useTheme();
return (
<TouchableOpacity
style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border, ...shadows.medium }]}
onPress={onPress}
activeOpacity={0.8}
>
<View style={styles.cardHeader}>
<View style={styles.cardTitleRow}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]} numberOfLines={1}>
{opportunity.Name}
</Text>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(opportunity.StageName, colors) }]}>
<Text style={[styles.statusText, { color: colors.surface, fontFamily: fonts.medium }]}>
{opportunity.StageName}
</Text>
</View>
</View>
{opportunity.Type && (
<Text style={[styles.cardSubtitle, { color: colors.textLight, fontFamily: fonts.regular }]} numberOfLines={1}>
{opportunity.Type}
</Text>
)}
</View>
<View style={styles.cardContent}>
<View style={styles.infoRow}>
<Icon name="currency-usd" size={16} color={colors.primary} />
<Text style={[styles.infoText, { color: colors.primary, fontFamily: fonts.bold }]}>
${opportunity.Amount?.toLocaleString()}
</Text>
</View>
<View style={styles.infoRow}>
<Icon name="trending-up" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
Probability: {opportunity.Probability}%
</Text>
</View>
<View style={styles.infoRow}>
<Icon name="calendar-outline" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
Close date: {new Date(opportunity.CloseDate).toLocaleDateString()}
</Text>
</View>
{opportunity.ForecastCategory && (
<View style={styles.infoRow}>
<Icon name="chart-line" size={16} color={colors.textLight} />
<Text style={[styles.infoText, { color: colors.text, fontFamily: fonts.regular }]}>
Forecast: {opportunity.ForecastCategory}
</Text>
</View>
)}
</View>
<View style={styles.cardFooter}>
<Text style={[styles.dateText, { color: colors.textLight, fontFamily: fonts.regular }]}>
Created: {new Date(opportunity.CreatedDate).toLocaleDateString()}
</Text>
</View>
</TouchableOpacity>
);
};
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,
},
});

View File

@ -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';

View File

@ -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<AppDispatch>();
// 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 (
<ScrollView
style={[styles.container, { backgroundColor: colors.background }]}
contentContainerStyle={styles.scrollContent}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
>
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>Salesforce CRM</Text>
<TouchableOpacity
style={[styles.dataButton, { backgroundColor: colors.primary }]}
onPress={() => navigation.navigate('SalesforceData' as never)}
>
<Icon name="database" size={20} color={colors.surface} />
<Text style={[styles.dataButtonText, { color: colors.surface, fontFamily: fonts.medium }]}>
View Data
</Text>
</TouchableOpacity>
</View>
{/* Loading State */}
{isLoading && !dashboardData.events.length && (
<View style={[styles.loadingCard, { borderColor: colors.primary, backgroundColor: colors.surface }]}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={[styles.loadingText, { color: colors.primary, fontFamily: fonts.medium }]}>
Loading Salesforce CRM data...
</Text>
</View>
)}
{/* Error State */}
{hasError && !dashboardData.events.length && (
<View style={[styles.errorCard, { borderColor: colors.error, backgroundColor: colors.surface }]}>
<Icon name="alert-circle" size={20} color={colors.error} />
<Text style={[styles.errorText, { color: colors.error, fontFamily: fonts.medium }]}>
Failed to load data. Pull to refresh.
</Text>
</View>
)}
{/* Hero Stats - Large Cards */}
<View style={styles.heroStatsContainer}>
<View style={[styles.heroCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.heroCardHeader}>
<Icon name="account-heart" size={24} color="#3B82F6" />
<Text style={[styles.heroCardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Leads</Text>
</View>
<Text style={[styles.heroCardValue, { color: colors.text, fontFamily: fonts.bold }]}>
{dashboardData.counts?.leads || dashboardData.summary.totalLeads}
</Text>
<Text style={[styles.heroCardSubtitle, { color: colors.textLight, fontFamily: fonts.regular }]}>
{dashboardData.summary.newLeads} New · {dashboardData.summary.qualifiedLeads} Qualified
</Text>
</View>
<View style={[styles.heroCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.heroCardHeader}>
<Icon name="handshake" size={24} color="#10B981" />
<Text style={[styles.heroCardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Opportunities</Text>
</View>
<Text style={[styles.heroCardValue, { color: colors.text, fontFamily: fonts.bold }]}>
{dashboardData.counts?.opportunities || dashboardData.summary.totalOpportunities}
</Text>
<Text style={[styles.heroCardSubtitle, { color: colors.textLight, fontFamily: fonts.regular }]}>
{dashboardData.summary.openOpportunities} Open · {dashboardData.summary.wonOpportunities} Won
</Text>
</View>
</View>
{/* Pipeline Value Card */}
<View style={[styles.pipelineCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.pipelineHeader}>
<Icon name="trending-up" size={28} color="#10B981" />
<Text style={[styles.pipelineTitle, { color: colors.text, fontFamily: fonts.bold }]}>Pipeline Overview</Text>
</View>
<View style={styles.pipelineStats}>
<View style={styles.pipelineItem}>
<Text style={[styles.pipelineLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Total Pipeline</Text>
<Text style={[styles.pipelineValue, { color: colors.text, fontFamily: fonts.bold }]}>
${(dashboardData.summary.totalPipelineValue / 1000).toFixed(1)}K
</Text>
</View>
<View style={styles.pipelineItem}>
<Text style={[styles.pipelineLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Avg Deal Size</Text>
<Text style={[styles.pipelineValue, { color: colors.primary, fontFamily: fonts.bold }]}>
${(dashboardData.summary.averageDealSize / 1000).toFixed(1)}K
</Text>
</View>
<View style={styles.pipelineItem}>
<Text style={[styles.pipelineLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Open Deals</Text>
<Text style={[styles.pipelineValue, { color: colors.text, fontFamily: fonts.bold }]}>
{dashboardData.summary.openOpportunities}
</Text>
</View>
</View>
</View>
{/* Module Counts - Compact Grid */}
<View style={styles.compactGrid}>
<View style={[styles.compactCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Icon name="calendar" size={20} color="#8B5CF6" />
<Text style={[styles.compactValue, { color: colors.text, fontFamily: fonts.bold }]}>
{dashboardData.counts?.events || dashboardData.summary.totalEvents}
</Text>
<Text style={[styles.compactLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Events</Text>
</View>
<View style={[styles.compactCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Icon name="check-circle" size={20} color="#10B981" />
<Text style={[styles.compactValue, { color: colors.text, fontFamily: fonts.bold }]}>
{dashboardData.counts?.tasks || dashboardData.summary.totalTasks}
</Text>
<Text style={[styles.compactLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Tasks</Text>
</View>
<View style={[styles.compactCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Icon name="domain" size={20} color="#F59E0B" />
<Text style={[styles.compactValue, { color: colors.text, fontFamily: fonts.bold }]}>
{dashboardData.counts?.accounts || dashboardData.summary.totalAccounts}
</Text>
<Text style={[styles.compactLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Accounts</Text>
</View>
</View>
{/* Lead Status Distribution */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Lead Status Distribution</Text>
<View style={styles.chartContainer}>
<PieChart
data={[
{ 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)}
colors={colors}
fonts={fonts}
size={140}
/>
{/* Legend */}
<View style={styles.pieLegend}>
{[
{ 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) => (
<View key={item.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: item.color }]} />
<Text style={[styles.legendText, { color: colors.textLight, fontFamily: fonts.regular }]}>
{item.label} ({item.value})
</Text>
</View>
))}
</View>
</View>
</View>
{/* Task Status */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Task Status</Text>
<View style={styles.chartContainer}>
<StackedBarChart
data={[
{ 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)}
colors={colors}
fonts={fonts}
height={120}
/>
{/* Legend */}
<View style={styles.barLegend}>
{[
{ 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) => (
<View key={item.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: item.color }]} />
<Text style={[styles.legendText, { color: colors.textLight, fontFamily: fonts.regular }]}>
{item.label} ({item.value})
</Text>
</View>
))}
</View>
</View>
</View>
{/* Key Metrics */}
<View style={styles.metricsRow}>
<View style={[styles.metricCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Icon name="calendar-clock" size={24} color="#F59E0B" />
<Text style={[styles.metricValue, { color: colors.text, fontFamily: fonts.bold }]}>
{dashboardData.summary.upcomingEvents}
</Text>
<Text style={[styles.metricLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Upcoming Events</Text>
</View>
<View style={[styles.metricCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Icon name="trophy" size={24} color="#22C55E" />
<Text style={[styles.metricValue, { color: colors.text, fontFamily: fonts.bold }]}>
{dashboardData.summary.wonOpportunities}
</Text>
<Text style={[styles.metricLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Won Deals</Text>
</View>
</View>
</ScrollView>
);
};
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;

View File

@ -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<AppDispatch>();
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 (
<View style={styles.footerLoader}>
<ActivityIndicator size="small" color={colors.primary} />
<Text style={[styles.footerText, { color: colors.textLight }]}>
Loading more...
</Text>
</View>
);
}, [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: (
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
),
};
switch (selectedTab) {
case 'events':
return (
<FlatList
data={events}
renderItem={({ item }) => (
<EventCard
event={item}
onPress={() => handleCardPress(item, 'Event')}
/>
)}
keyExtractor={(item) => `event-${item.Id}`}
onEndReached={() => loadMoreData('events')}
onEndReachedThreshold={0.1}
ListFooterComponent={() => renderFooter('events')}
{...commonFlatListProps}
/>
);
case 'leads':
return (
<FlatList
data={leads}
renderItem={({ item }) => (
<LeadCard
lead={item}
onPress={() => handleCardPress(item, 'Lead')}
/>
)}
keyExtractor={(item) => `lead-${item.Id}`}
onEndReached={() => loadMoreData('leads')}
onEndReachedThreshold={0.1}
ListFooterComponent={() => renderFooter('leads')}
{...commonFlatListProps}
/>
);
case 'tasks':
return (
<FlatList
data={tasks}
renderItem={({ item }) => (
<TaskCard
task={item}
onPress={() => handleCardPress(item, 'Task')}
/>
)}
keyExtractor={(item) => `task-${item.Id}`}
onEndReached={() => loadMoreData('tasks')}
onEndReachedThreshold={0.1}
ListFooterComponent={() => renderFooter('tasks')}
{...commonFlatListProps}
/>
);
case 'accounts':
return (
<FlatList
data={accounts}
renderItem={({ item }) => (
<AccountCard
account={item}
onPress={() => handleCardPress(item, 'Account')}
/>
)}
keyExtractor={(item) => `account-${item.Id}`}
onEndReached={() => loadMoreData('accounts')}
onEndReachedThreshold={0.1}
ListFooterComponent={() => renderFooter('accounts')}
{...commonFlatListProps}
/>
);
case 'opportunities':
return (
<FlatList
data={opportunities}
renderItem={({ item }) => (
<OpportunityCard
opportunity={item}
onPress={() => 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 <LoadingSpinner />;
}
if (hasError && !leads.length) {
return <ErrorState onRetry={handleRetry} />;
}
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
{/* Fixed Header */}
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
Salesforce CRM Data
</Text>
<TouchableOpacity onPress={handleRefresh} disabled={refreshing}>
<Icon name="refresh" size={24} color={colors.primary} />
</TouchableOpacity>
</View>
{/* Fixed Tabs */}
<View style={styles.tabsContainer}>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.tabs}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.key}
style={[
styles.tab,
selectedTab === tab.key && { backgroundColor: colors.primary },
]}
onPress={() => setSelectedTab(tab.key)}
activeOpacity={0.8}
>
<Icon
name={tab.icon}
size={20}
color={selectedTab === tab.key ? colors.surface : colors.textLight}
/>
<Text
style={[
styles.tabText,
{
color: selectedTab === tab.key ? colors.surface : colors.textLight,
fontFamily: fonts.medium,
},
]}
>
{tab.label}
</Text>
<View
style={[
styles.countBadge,
{ backgroundColor: selectedTab === tab.key ? colors.surface : colors.primary },
]}
>
<Text
style={[
styles.countText,
{
color: selectedTab === tab.key ? colors.primary : colors.surface,
fontFamily: fonts.bold,
},
]}
>
{tab.count}
</Text>
</View>
</TouchableOpacity>
))}
</View>
</ScrollView>
</View>
{/* Scrollable Content */}
<View style={styles.content}>
{renderTabContent()}
</View>
</View>
);
};
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;

View File

@ -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: <T = any>(
resource: SalesforceResourceType,
params?: SalesforceSearchParams
) => {
const queryParams = {
page: params?.page || 1,
limit: params?.limit || 20,
...params
};
return http.get<SalesforceApiResponse<T>>(`${SALESFORCE_CRM_BASE_URL}/${resource}`, queryParams);
},
// Specific resource methods for type safety
getEvents: (params?: SalesforceSearchParams) =>
salesforceCrmAPI.getSalesforceCrmData<SalesforceEvent>('events', params),
getLeads: (params?: SalesforceSearchParams) =>
salesforceCrmAPI.getSalesforceCrmData<SalesforceLead>('leads', params),
getTasks: (params?: SalesforceSearchParams) =>
salesforceCrmAPI.getSalesforceCrmData<SalesforceTask>('tasks', params),
getAccounts: (params?: SalesforceSearchParams) =>
salesforceCrmAPI.getSalesforceCrmData<SalesforceAccount>('accounts', params),
getOpportunities: (params?: SalesforceSearchParams) =>
salesforceCrmAPI.getSalesforceCrmData<SalesforceOpportunity>('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`);
},
};

View File

@ -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;

View File

@ -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
);

View File

@ -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<T> {
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<T> {
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<string, number>;
};
leads: {
total: number;
new: number;
working: number;
qualified: number;
byStatus: Record<LeadStatus, number>;
bySource: Record<string, number>;
};
tasks: {
total: number;
open: number;
completed: number;
overdue: number;
byStatus: Record<TaskStatus, number>;
byPriority: Record<TaskPriority, number>;
};
accounts: {
total: number;
byIndustry: Record<string, number>;
byType: Record<string, number>;
totalRevenue: number;
};
opportunities: {
total: number;
open: number;
won: number;
lost: number;
totalValue: number;
averageDealSize: number;
byStage: Record<OpportunityStage, number>;
pipelineValue: number;
winRate: number;
};
}

View File

@ -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<Props> = ({ route }) => {
const dispatch = useDispatch<AppDispatch>();
const navigation = useNavigation();
const [showZohoAuth, setShowZohoAuth] = React.useState(false);
const [showSalesforceAuth, setShowSalesforceAuth] = React.useState(false);
const [pendingService, setPendingService] = React.useState<string | null>(null);
const [isCheckingToken, setIsCheckingToken] = React.useState(false);
const [authenticatedServices, setAuthenticatedServices] = React.useState<Set<string>>(new Set());
@ -102,11 +104,50 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ route }) => {
ItemSeparatorComponent={() => <View style={[styles.sep, { backgroundColor: colors.border }]} />}
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 (
<View style={styles.serviceItem}>
@ -142,8 +191,10 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ 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<Props> = ({ route }) => {
)}
</TouchableOpacity>
{/* Re-authentication button for Zoho services - always visible */}
{requiresZohoAuth && (
{/* Re-authentication button for services that require auth */}
{requiresAuth && (
<TouchableOpacity
style={[styles.reauthButton, { backgroundColor: colors.background, borderColor: colors.border }]}
onPress={() => handleReAuthenticate(item.key)}
@ -190,7 +241,7 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
<ZohoAuth
serviceKey={pendingService as any}
onAuthSuccess={(authData) => {
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<Props> = ({ route }) => {
}}
/>
</Modal>
{/* Salesforce Auth Modal */}
<Modal
visible={showSalesforceAuth}
animationType="slide"
presentationStyle="fullScreen"
onRequestClose={() => setShowSalesforceAuth(false)}
>
<SalesforceAuth
onAuthSuccess={(authData) => {
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);
}}
/>
</Modal>
</View>
);
};

View File

@ -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(`
<!DOCTYPE html>
<html>
<head>
<title>Authentication Successful</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
text-align: center;
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.success-icon {
font-size: 64px;
color: #10b981;
margin-bottom: 20px;
}
h1 {
color: #1f2937;
margin-bottom: 10px;
}
p {
color: #6b7280;
}
</style>
</head>
<body>
<div class="container">
<div class="success-icon"></div>
<h1>Authentication Successful!</h1>
<p>Your Salesforce account has been connected.</p>
<p>You can close this window now.</p>
</div>
<script>
// Mobile app will detect this URL and close the WebView
window.location.search = '?status=success';
</script>
</body>
</html>
`);
```
##### 5. Handle Errors
```javascript
// If token exchange or storage fails
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Authentication Failed</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
}
.container {
text-align: center;
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.error-icon {
font-size: 64px;
color: #ef4444;
margin-bottom: 20px;
}
h1 {
color: #1f2937;
margin-bottom: 10px;
}
p {
color: #6b7280;
}
</style>
</head>
<body>
<div class="container">
<div class="error-icon"></div>
<h1>Authentication Failed</h1>
<p>${errorMessage || 'An error occurred during authentication.'}</p>
<p>Please try again.</p>
</div>
<script>
window.location.search = '?status=error&message=${encodeURIComponent(errorMessage)}';
</script>
</body>
</html>
`);
```
---
## 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 `
<!DOCTYPE html>
<html>
<head>
<title>Success</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.container { background: white; padding: 40px; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); display: inline-block; }
.icon { font-size: 64px; color: #10b981; margin-bottom: 20px; }
h1 { color: #1f2937; }
p { color: #6b7280; }
</style>
</head>
<body>
<div class="container">
<div class="icon"></div>
<h1>Authentication Successful!</h1>
<p>Your Salesforce account has been connected.</p>
</div>
<script>
setTimeout(() => { window.location.search = '?status=success'; }, 500);
</script>
</body>
</html>
`;
}
// Error page HTML
function errorPage(message) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Error</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: linear-gradient(135deg, #f87171 0%, #ef4444 100%); }
.container { background: white; padding: 40px; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); display: inline-block; }
.icon { font-size: 64px; color: #ef4444; margin-bottom: 20px; }
h1 { color: #1f2937; }
p { color: #6b7280; }
</style>
</head>
<body>
<div class="container">
<div class="icon"></div>
<h1>Authentication Failed</h1>
<p>${message || 'An error occurred.'}</p>
</div>
<script>
setTimeout(() => { window.location.search = '?status=error&message=${encodeURIComponent(message)}'; }, 500);
</script>
</body>
</html>
`;
}
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)

View File

@ -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
<!-- android/app/src/main/AndroidManifest.xml -->
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTask">
<!-- Existing intent filters -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Salesforce OAuth Callback -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="centralizedreportingsystem"
android:host="oauth"
android:pathPrefix="/salesforce/callback" />
</intent-filter>
</activity>
```
### Step 3: Configure Deep Linking (iOS)
#### iOS - Update Info.plist
Add URL types for the OAuth callback:
```xml
<!-- ios/CentralizedReportingSystem/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>centralizedreportingsystem</string>
</array>
<key>CFBundleURLName</key>
<string>com.yourcompany.centralizedreportingsystem</string>
</dict>
</array>
```
### 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

View File

@ -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<string, any> => {
const result: Record<string, any> = {};
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<SalesforceAuthProps> = ({
onAuthSuccess,
onAuthError,
onClose,
}) => {
const { colors, spacing, fonts, shadows } = useTheme();
const webViewRef = useRef<WebView>(null);
const navigation = useNavigation<any>();
const { user, accessToken } = useSelector((state: RootState) => state.auth);
const [state, setState] = useState<SalesforceAuthState>({
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<void>(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 (
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
{/* Header */}
<View style={[styles.header, { backgroundColor: colors.surface, ...shadows.light }]}>
<View style={styles.headerContent}>
<Icon name="cloud" size={24} color="#00A1E0" />
<Text style={[styles.headerTitle, { color: colors.text, fontFamily: fonts.medium }]}>
Salesforce Authentication
</Text>
</View>
<TouchableOpacity
style={[styles.closeButton, { backgroundColor: colors.background }]}
onPress={handleClose}
activeOpacity={0.7}
>
<Icon name="close" size={20} color={colors.text} />
</TouchableOpacity>
</View>
{/* Error State */}
{state.error && (
<View style={[styles.errorContainer, { backgroundColor: colors.surface }]}>
<Icon name="error-outline" size={48} color={colors.error} />
<Text style={[styles.errorTitle, { color: colors.text, fontFamily: fonts.medium }]}>
Authentication Failed
</Text>
<Text style={[styles.errorMessage, { color: colors.textLight, fontFamily: fonts.regular }]}>
{state.error}
</Text>
<TouchableOpacity
style={[styles.retryButton, { backgroundColor: '#00A1E0' }]}
onPress={handleReload}
activeOpacity={0.8}
>
<Text style={[styles.retryButtonText, { color: colors.surface, fontFamily: fonts.medium }]}>
Try Again
</Text>
</TouchableOpacity>
</View>
)}
{/* Processing Modal */}
{state.processing && (
<View style={[styles.processingOverlay, { backgroundColor: 'rgba(0, 0, 0, 0.7)' }]}>
<View style={[styles.processingModal, { backgroundColor: colors.surface }]}>
<View style={styles.processingIconContainer}>
<ActivityIndicator size="large" color="#00A1E0" />
</View>
<Text style={[styles.processingTitle, { color: colors.text, fontFamily: fonts.bold }]}>
Setting Up Salesforce
</Text>
<Text style={[styles.processingStep, { color: colors.textLight, fontFamily: fonts.regular }]}>
{state.processingStep}
</Text>
<View style={styles.processingDots}>
<View style={[styles.dot, { backgroundColor: '#00A1E0' }]} />
<View style={[styles.dot, { backgroundColor: '#00A1E0' }]} />
<View style={[styles.dot, { backgroundColor: '#00A1E0' }]} />
</View>
</View>
</View>
)}
{/* Loading Overlay */}
{state.loading && !state.error && (
<View style={[styles.loadingOverlay, { backgroundColor: colors.background }]}>
<ActivityIndicator size="large" color="#00A1E0" />
<Text style={[styles.loadingText, { color: colors.textLight, fontFamily: fonts.regular }]}>
Loading Salesforce Login...
</Text>
</View>
)}
{/* WebView */}
{!state.error && !state.processing && (
<WebView
ref={webViewRef}
source={{ uri: state.currentUrl }}
style={styles.webView}
onNavigationStateChange={handleNavigationStateChange}
onLoadStart={handleLoadStart}
onLoadEnd={handleLoadEnd}
onError={handleWebViewError}
onHttpError={handleWebViewError}
startInLoadingState={true}
javaScriptEnabled={true}
domStorageEnabled={true}
sharedCookiesEnabled={true}
thirdPartyCookiesEnabled={true}
mixedContentMode="compatibility"
originWhitelist={["*"]}
allowsInlineMediaPlayback={true}
mediaPlaybackRequiresUserAction={false}
onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
setSupportMultipleWindows={false}
javaScriptCanOpenWindowsAutomatically={true}
renderLoading={() => (
<View style={[styles.webViewLoading, { backgroundColor: colors.background }]}>
<ActivityIndicator size="large" color="#00A1E0" />
<Text style={[styles.webViewLoadingText, { color: colors.textLight, fontFamily: fonts.regular }]}>
Loading...
</Text>
</View>
)}
/>
)}
</SafeAreaView>
);
};
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;

View File

@ -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,
});

View File

@ -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'],
};