webview login for zoho implemented
This commit is contained in:
parent
bcaa1fbdaa
commit
70b9198aa4
26
App.tsx
26
App.tsx
@ -22,6 +22,28 @@ import { StatusBar } from 'react-native';
|
||||
function AppContent(): React.JSX.Element {
|
||||
const isAuthenticated = useSelector((s: RootState) => Boolean(s.auth.token));
|
||||
const selectedService = useSelector((s: RootState) => s.integrations.selectedService);
|
||||
const linking = {
|
||||
prefixes: ['centralizedreportingsystem://', 'https://centralizedreportingsystem.com'],
|
||||
config: {
|
||||
screens: {
|
||||
// App tabs (see src/navigation/AppNavigator.tsx)
|
||||
Dashboard: {
|
||||
path: 'dashboard',
|
||||
},
|
||||
Profile: {
|
||||
path: 'profile',
|
||||
screens: {
|
||||
Profile: {
|
||||
path: ':userId?',
|
||||
parse: {
|
||||
userId: (userId: string) => (userId ? parseInt(userId, 10) : undefined),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<PersistGate loading={<LoadingSpinner />} persistor={persistor}>
|
||||
@ -32,11 +54,13 @@ function AppContent(): React.JSX.Element {
|
||||
</NavigationContainer>
|
||||
) : (
|
||||
!selectedService ? (
|
||||
<NavigationContainer>
|
||||
<NavigationContainer linking={linking} >
|
||||
<IntegrationsNavigator/>
|
||||
</NavigationContainer>
|
||||
) : (
|
||||
<NavigationContainer linking={linking} >
|
||||
<AppNavigator />
|
||||
</NavigationContainer>
|
||||
)
|
||||
)}
|
||||
</PersistGate>
|
||||
|
||||
@ -21,6 +21,27 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!-- Existing intent filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deep link intent filter -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<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" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Universal link intent filter -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="centralizedreportingsystem.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
@ -33,6 +33,7 @@
|
||||
"react-native-svg": "^15.12.1",
|
||||
"react-native-toast-message": "^2.2.1",
|
||||
"react-native-vector-icons": "^10.3.0",
|
||||
"react-native-webview": "^13.16.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"redux-persist": "^6.0.0"
|
||||
},
|
||||
@ -11544,6 +11545,20 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webview": {
|
||||
"version": "13.16.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.0.tgz",
|
||||
"integrity": "sha512-Nh13xKZWW35C0dbOskD7OX01nQQavOzHbCw9XoZmar4eXCo7AvrYJ0jlUfRVVIJzqINxHlpECYLdmAdFsl9xDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"invariant": "2.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
|
||||
"version": "0.79.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.0.tgz",
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
"react-native-svg": "^15.12.1",
|
||||
"react-native-toast-message": "^2.2.1",
|
||||
"react-native-vector-icons": "^10.3.0",
|
||||
"react-native-webview": "^13.16.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"redux-persist": "^6.0.0"
|
||||
},
|
||||
|
||||
@ -7,6 +7,8 @@ import type { IntegrationsStackParamList } from '@/modules/integrations/navigati
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setSelectedService } from '@/modules/integrations/store/integrationsSlice';
|
||||
import type { AppDispatch } from '@/store/store';
|
||||
import ZohoAuth from './ZohoAuth';
|
||||
import { Modal } from 'react-native';
|
||||
|
||||
type Route = RouteProp<IntegrationsStackParamList, 'IntegrationCategory'>;
|
||||
|
||||
@ -55,6 +57,8 @@ interface Props {
|
||||
const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
|
||||
const { colors, fonts } = useTheme();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const [showZohoAuth, setShowZohoAuth] = React.useState(false);
|
||||
const [pendingService, setPendingService] = React.useState<string | null>(null);
|
||||
const services = servicesMap[route.params.categoryKey] ?? [];
|
||||
|
||||
return (
|
||||
@ -68,7 +72,17 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
|
||||
<TouchableOpacity
|
||||
style={styles.row}
|
||||
activeOpacity={0.8}
|
||||
onPress={() => dispatch(setSelectedService(item.key))}
|
||||
onPress={() => {
|
||||
// Here we decide whether to require Zoho authentication first
|
||||
const requiresZohoAuth = item.key === 'zohoProjects' || item.key === 'zohoPeople' || item.key === 'zohoBooks' || item.key === 'zohoCRM';
|
||||
console.log('key pressed',item.key)
|
||||
if (requiresZohoAuth) {
|
||||
setPendingService(item.key);
|
||||
setShowZohoAuth(true);
|
||||
} else {
|
||||
dispatch(setSelectedService(item.key));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={[styles.iconCircle, { backgroundColor: '#F1F5F9' }]}>
|
||||
<Icon name={item.icon} size={20} color={colors.primary} />
|
||||
@ -77,6 +91,34 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Zoho Auth Modal */}
|
||||
<Modal
|
||||
visible={showZohoAuth}
|
||||
animationType="slide"
|
||||
presentationStyle="fullScreen"
|
||||
onRequestClose={() => setShowZohoAuth(false)}
|
||||
>
|
||||
<ZohoAuth
|
||||
serviceKey={pendingService as any}
|
||||
onAuthSuccess={(authData) => {
|
||||
console.log('auth data i got',authData)
|
||||
setShowZohoAuth(false);
|
||||
if (pendingService) {
|
||||
dispatch(setSelectedService(pendingService));
|
||||
setPendingService(null);
|
||||
}
|
||||
}}
|
||||
onAuthError={() => {
|
||||
setShowZohoAuth(false);
|
||||
setPendingService(null);
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowZohoAuth(false);
|
||||
setPendingService(null);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
159
src/modules/integrations/screens/README.md
Normal file
159
src/modules/integrations/screens/README.md
Normal file
@ -0,0 +1,159 @@
|
||||
# Zoho Authentication WebView
|
||||
|
||||
This directory contains the Zoho authentication implementation using React Native WebView.
|
||||
|
||||
## Files
|
||||
|
||||
- `ZohoAuth.tsx` - Main WebView component for Zoho OAuth authentication
|
||||
- `ZohoAuthExample.tsx` - Example implementation showing how to use the ZohoAuth component
|
||||
- `LoginScreen.tsx` - Existing login screen (can be extended to include Zoho auth)
|
||||
|
||||
## ZohoAuth Component
|
||||
|
||||
The `ZohoAuth` component provides a WebView-based authentication flow for Zoho services.
|
||||
|
||||
### Features
|
||||
|
||||
- ✅ WebView-based OAuth flow
|
||||
- ✅ Error handling and retry functionality
|
||||
- ✅ Loading states and user feedback
|
||||
- ✅ Theme integration with the app's design system
|
||||
- ✅ TypeScript support with proper type definitions
|
||||
- ✅ Responsive design with proper styling
|
||||
|
||||
### Usage
|
||||
|
||||
```tsx
|
||||
import React, { useState } from 'react';
|
||||
import { Modal } from 'react-native';
|
||||
import ZohoAuth from './ZohoAuth';
|
||||
|
||||
const MyComponent = () => {
|
||||
const [showZohoAuth, setShowZohoAuth] = useState(false);
|
||||
|
||||
const handleAuthSuccess = (authData) => {
|
||||
console.log('Auth successful:', authData);
|
||||
// Handle successful authentication
|
||||
setShowZohoAuth(false);
|
||||
};
|
||||
|
||||
const handleAuthError = (error) => {
|
||||
console.error('Auth failed:', error);
|
||||
// Handle authentication error
|
||||
setShowZohoAuth(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button title="Login with Zoho" onPress={() => setShowZohoAuth(true)} />
|
||||
|
||||
<Modal visible={showZohoAuth} animationType="slide">
|
||||
<ZohoAuth
|
||||
onAuthSuccess={handleAuthSuccess}
|
||||
onAuthError={handleAuthError}
|
||||
onClose={() => setShowZohoAuth(false)}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Before using the component, you need to configure your Zoho OAuth credentials:
|
||||
|
||||
1. **Update the configuration in `ZohoAuth.tsx`:**
|
||||
|
||||
```typescript
|
||||
const ZOHO_CONFIG = {
|
||||
CLIENT_ID: 'YOUR_ZOHO_CLIENT_ID', // Your Zoho OAuth app client ID
|
||||
REDIRECT_URI: 'YOUR_REDIRECT_URI', // Your app's redirect URI
|
||||
SCOPE: 'ZohoProjects.projects.READ,ZohoProjects.tasks.READ,ZohoProjects.timesheets.READ',
|
||||
RESPONSE_TYPE: 'code',
|
||||
ACCESS_TYPE: 'offline',
|
||||
PROMPT: 'consent',
|
||||
};
|
||||
```
|
||||
|
||||
2. **Set up your Zoho OAuth application:**
|
||||
- Go to [Zoho Developer Console](https://api-console.zoho.com/)
|
||||
- Create a new OAuth application
|
||||
- Set the redirect URI to match your app's scheme (e.g., `com.yourapp://oauth/callback`)
|
||||
- Copy the Client ID to the configuration
|
||||
|
||||
3. **Configure deep linking in your app:**
|
||||
- Add URL scheme handling to your app's configuration
|
||||
- Ensure the redirect URI matches your app's scheme
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `onAuthSuccess` | `(authData: ZohoAuthData) => void` | No | Callback when authentication succeeds |
|
||||
| `onAuthError` | `(error: string) => void` | No | Callback when authentication fails |
|
||||
| `onClose` | `() => void` | No | Callback when user closes the auth flow |
|
||||
|
||||
### Auth Data Structure
|
||||
|
||||
```typescript
|
||||
interface ZohoAuthData {
|
||||
accessToken: string; // OAuth access token
|
||||
refreshToken?: string; // OAuth refresh token (if available)
|
||||
expiresIn?: number; // Token expiration time in seconds
|
||||
scope?: string; // Granted scopes
|
||||
tokenType?: string; // Token type (usually 'Bearer')
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
The component handles various error scenarios:
|
||||
|
||||
- Network connectivity issues
|
||||
- Invalid OAuth configuration
|
||||
- User cancellation
|
||||
- Zoho API errors
|
||||
- WebView loading errors
|
||||
|
||||
All errors are passed to the `onAuthError` callback with descriptive error messages.
|
||||
|
||||
### Styling
|
||||
|
||||
The component uses the app's theme system and automatically adapts to light/dark modes. It includes:
|
||||
|
||||
- Consistent color scheme
|
||||
- Proper spacing and typography
|
||||
- Loading indicators
|
||||
- Error states with retry options
|
||||
- Responsive design
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- The component uses OAuth 2.0 authorization code flow
|
||||
- Authorization codes should be exchanged for access tokens on your backend server
|
||||
- Never store sensitive tokens in the app's local storage without encryption
|
||||
- Use secure storage solutions for token persistence
|
||||
|
||||
### Example Integration
|
||||
|
||||
See `ZohoAuthExample.tsx` for a complete example of how to integrate the ZohoAuth component into your app, including:
|
||||
|
||||
- Modal presentation
|
||||
- Success/error handling
|
||||
- UI state management
|
||||
- User feedback
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `react-native-webview` - For the WebView component
|
||||
- `react-native-vector-icons` - For icons
|
||||
- `@shared/styles/useTheme` - For theme integration
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Configure your Zoho OAuth credentials
|
||||
2. Set up deep linking in your app
|
||||
3. Implement backend token exchange
|
||||
4. Add token storage and refresh logic
|
||||
5. Integrate with your app's authentication flow
|
||||
481
src/modules/integrations/screens/ZohoAuth.tsx
Normal file
481
src/modules/integrations/screens/ZohoAuth.tsx
Normal file
@ -0,0 +1,481 @@
|
||||
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';
|
||||
|
||||
// Types
|
||||
type ServiceKey = 'zohoProjects' | 'zohoCRM' | 'zohoBooks' | 'zohoPeople';
|
||||
|
||||
interface ZohoAuthProps {
|
||||
serviceKey?: ServiceKey;
|
||||
onAuthSuccess?: (authData: ZohoAuthData) => void;
|
||||
onAuthError?: (error: string) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
interface ZohoAuthData {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
expiresIn?: number;
|
||||
scope?: string;
|
||||
tokenType?: string;
|
||||
}
|
||||
|
||||
interface ZohoAuthState {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
currentUrl: string;
|
||||
}
|
||||
|
||||
// Zoho OAuth Configuration
|
||||
const ZOHO_CONFIG = {
|
||||
// Replace with your actual Zoho OAuth app credentials
|
||||
CLIENT_ID: '1000.PY0FB5ABLLFK2WIBDCNCGKQ0EUIJMY',
|
||||
REDIRECT_URI: 'centralizedreportingsystem://oauth/callback', // Must match Zoho console exactly
|
||||
ACCOUNTS_BASE_URL: 'https://accounts.zoho.com', // Change to region if needed (e.g., .eu, .in)
|
||||
RESPONSE_TYPE: 'code',
|
||||
ACCESS_TYPE: 'offline',
|
||||
PROMPT: 'consent',
|
||||
};
|
||||
|
||||
// Unified scope: request all needed product scopes in one consent, so the issued token works
|
||||
// across Projects, CRM, Books, and People. Tailor scopes to your app's needs and compliance.
|
||||
const getScopeForService = (_serviceKey?: ServiceKey): string => {
|
||||
return [
|
||||
// Zoho Projects
|
||||
'ZohoProjects.projects.READ',
|
||||
'ZohoProjects.tasks.READ',
|
||||
'ZohoProjects.timesheets.READ',
|
||||
// Zoho CRM (adjust modules per your needs)
|
||||
'ZohoCRM.users.READ',
|
||||
'ZohoCRM.modules.READ',
|
||||
// Zoho Books (use granular scopes if preferred instead of FullAccess)
|
||||
'ZohoBooks.FullAccess.READ',
|
||||
// Zoho People
|
||||
'ZohoPeople.employee.READ',
|
||||
].join(',');
|
||||
};
|
||||
|
||||
// Build Zoho OAuth URL
|
||||
const buildZohoAuthUrl = (scope: string): string => {
|
||||
const baseUrl = `${ZOHO_CONFIG.ACCOUNTS_BASE_URL}/oauth/v2/auth`;
|
||||
const params = new URLSearchParams({
|
||||
client_id: ZOHO_CONFIG.CLIENT_ID,
|
||||
redirect_uri: ZOHO_CONFIG.REDIRECT_URI,
|
||||
scope,
|
||||
response_type: ZOHO_CONFIG.RESPONSE_TYPE,
|
||||
access_type: ZOHO_CONFIG.ACCESS_TYPE,
|
||||
prompt: ZOHO_CONFIG.PROMPT,
|
||||
});
|
||||
|
||||
return `${baseUrl}?${params.toString()}`;
|
||||
};
|
||||
|
||||
// Safe query param parser for React Native (avoids relying on DOM URL types)
|
||||
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 redirect URI
|
||||
const isRedirectUri = (url: string): boolean => {
|
||||
return url.startsWith(ZOHO_CONFIG.REDIRECT_URI);
|
||||
};
|
||||
|
||||
const ZohoAuth: React.FC<ZohoAuthProps> = ({
|
||||
serviceKey,
|
||||
onAuthSuccess,
|
||||
onAuthError,
|
||||
onClose,
|
||||
}) => {
|
||||
const { colors, spacing, fonts, shadows } = useTheme();
|
||||
const webViewRef = useRef<WebView>(null);
|
||||
const navigation = useNavigation<any>();
|
||||
|
||||
const currentScope = useMemo(() => getScopeForService(serviceKey), [serviceKey]);
|
||||
|
||||
const [state, setState] = useState<ZohoAuthState>({
|
||||
loading: true,
|
||||
error: null,
|
||||
currentUrl: buildZohoAuthUrl(currentScope),
|
||||
});
|
||||
|
||||
// Backend exchange mode: only log and return the authorization code to the caller
|
||||
const handleAuthorizationCode = useCallback((authCode: string) => {
|
||||
console.log('[ZohoAuth] Authorization code received:', authCode);
|
||||
console.log('[ZohoAuth] Send this code to your backend to exchange for tokens.');
|
||||
// Return the code via onAuthSuccess using the existing shape
|
||||
onAuthSuccess?.({
|
||||
accessToken: authCode, // This is the AUTHORIZATION CODE, not an access token
|
||||
tokenType: 'authorization_code',
|
||||
scope: currentScope,
|
||||
expiresIn: undefined,
|
||||
refreshToken: undefined,
|
||||
});
|
||||
}, [onAuthSuccess, currentScope]);
|
||||
|
||||
// Handle WebView navigation state changes
|
||||
const handleNavigationStateChange = useCallback((navState: any) => {
|
||||
const { url, loading } = navState;
|
||||
const authCode = extractAuthCode(url);
|
||||
console.log('[ZohoAuth] nav change url:', url, 'loading:', loading, 'code:', authCode);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading,
|
||||
currentUrl: url,
|
||||
}));
|
||||
|
||||
// First: intercept redirect URI to capture code
|
||||
if (isRedirectUri(url) && authCode) {
|
||||
console.log('[ZohoAuth] redirect detected in nav change. Code captured.');
|
||||
handleAuthorizationCode(authCode);
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to tabs based on keywords in URL
|
||||
const lowerUrl = (url ?? '').toLowerCase();
|
||||
if (lowerUrl.includes('dashboard')) {
|
||||
// Close auth view if provided and navigate to Dashboard tab
|
||||
const params = getAllQueryParamsFromUrl(url || '');
|
||||
onClose?.();
|
||||
// Pass any available params to the Dashboard tab (component can consume via route.params)
|
||||
navigation.navigate('Dashboard' as never, params as never);
|
||||
return;
|
||||
}
|
||||
if (lowerUrl.includes('profile')) {
|
||||
// Close auth view if provided and navigate to Profile tab
|
||||
const params = getAllQueryParamsFromUrl(url || '');
|
||||
// Prefer `userId` param when present and numeric
|
||||
if (typeof params.userId === 'string' && /^-?\d+$/.test(params.userId)) {
|
||||
params.userId = Number(params.userId);
|
||||
}
|
||||
onClose?.();
|
||||
// Route into Profile stack's default screen with params
|
||||
navigation.navigate('Profile' as never, { screen: 'Profile', params } as never);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: if redirect URI without code or with error
|
||||
if (isRedirectUri(url) && !loading) {
|
||||
const err = getQueryParamFromUrl(url, 'error');
|
||||
if (err) {
|
||||
const errorDescription = getQueryParamFromUrl(url, 'error_description') || err;
|
||||
handleAuthError(errorDescription);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Intercept before WebView tries to handle custom scheme
|
||||
const handleShouldStartLoadWithRequest = useCallback((request: any) => {
|
||||
const { url } = request || {};
|
||||
if (!url) return true;
|
||||
if (isRedirectUri(url)) {
|
||||
const code = extractAuthCode(url);
|
||||
console.log('[ZohoAuth] onShouldStart intercept url:', url, 'code:', code);
|
||||
if (code) {
|
||||
handleAuthorizationCode(code);
|
||||
return false; // prevent navigation to custom scheme
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, [handleAuthorizationCode]);
|
||||
|
||||
// Handle successful authentication
|
||||
const handleAuthSuccess = useCallback(async (authCode: string) => {
|
||||
try {
|
||||
setState(prev => ({ ...prev, loading: true }));
|
||||
|
||||
// Here you would typically exchange the authorization code for access token
|
||||
// This should be done on your backend server for security
|
||||
const authData: ZohoAuthData = {
|
||||
accessToken: authCode, // In real implementation, this would be the actual access token
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
scope: currentScope,
|
||||
};
|
||||
|
||||
onAuthSuccess?.(authData);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Authentication failed';
|
||||
handleAuthError(errorMessage);
|
||||
}
|
||||
}, [onAuthSuccess, currentScope]);
|
||||
|
||||
// Handle authentication error
|
||||
const handleAuthError = useCallback((error: string) => {
|
||||
setState(prev => ({ ...prev, error, loading: false }));
|
||||
onAuthError?.(error);
|
||||
|
||||
Alert.alert(
|
||||
'Authentication Error',
|
||||
error,
|
||||
[
|
||||
{
|
||||
text: 'Retry',
|
||||
onPress: () => {
|
||||
setState(prev => ({ ...prev, error: null, currentUrl: buildZohoAuthUrl(currentScope) }));
|
||||
webViewRef.current?.reload();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Cancel',
|
||||
onPress: onClose,
|
||||
style: 'cancel',
|
||||
},
|
||||
]
|
||||
);
|
||||
}, [onAuthError, onClose]);
|
||||
|
||||
// Handle WebView error
|
||||
const handleWebViewError = useCallback((syntheticEvent: any) => {
|
||||
const { nativeEvent } = syntheticEvent;
|
||||
console.error('WebView error:', nativeEvent);
|
||||
handleAuthError('Failed to load authentication page. Please check your internet connection.');
|
||||
}, [handleAuthError]);
|
||||
|
||||
const handleLoadStart = useCallback((e: any) => {
|
||||
console.log('[ZohoAuth] load start:', e?.nativeEvent?.url);
|
||||
}, []);
|
||||
|
||||
const handleLoadEnd = useCallback((e: any) => {
|
||||
console.log('[ZohoAuth] 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: buildZohoAuthUrl(currentScope) }));
|
||||
webViewRef.current?.reload();
|
||||
}, [currentScope]);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
{/* Header */}
|
||||
<View style={[styles.header, { backgroundColor: colors.surface, ...shadows.light }]}>
|
||||
<View style={styles.headerContent}>
|
||||
<Icon name="account-circle" size={24} color={colors.primary} />
|
||||
<Text style={[styles.headerTitle, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||
Zoho 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: colors.primary }]}
|
||||
onPress={handleReload}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={[styles.retryButtonText, { color: colors.surface, fontFamily: fonts.medium }]}>
|
||||
Try Again
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Loading Overlay */}
|
||||
{state.loading && !state.error && (
|
||||
<View style={[styles.loadingOverlay, { backgroundColor: colors.background }]}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.textLight, fontFamily: fonts.regular }]}>
|
||||
Loading Zoho Login...
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* WebView */}
|
||||
{!state.error && (
|
||||
<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={colors.primary} />
|
||||
<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,
|
||||
},
|
||||
});
|
||||
|
||||
export default ZohoAuth;
|
||||
247
src/modules/integrations/screens/ZohoAuthExample.tsx
Normal file
247
src/modules/integrations/screens/ZohoAuthExample.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/MaterialIcons';
|
||||
import { useTheme } from '@/shared/styles/useTheme';
|
||||
import ZohoAuth from './ZohoAuth';
|
||||
|
||||
// Types
|
||||
interface ZohoAuthData {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
expiresIn?: number;
|
||||
scope?: string;
|
||||
tokenType?: string;
|
||||
}
|
||||
|
||||
const ZohoAuthExample: React.FC = () => {
|
||||
const { colors, spacing, fonts, shadows } = useTheme();
|
||||
const [showZohoAuth, setShowZohoAuth] = useState(false);
|
||||
const [authData, setAuthData] = useState<ZohoAuthData | null>(null);
|
||||
|
||||
// Handle successful Zoho authentication
|
||||
const handleZohoAuthSuccess = (data: ZohoAuthData) => {
|
||||
console.log('Zoho Auth Success:', data);
|
||||
setAuthData(data);
|
||||
setShowZohoAuth(false);
|
||||
|
||||
Alert.alert(
|
||||
'Authentication Successful',
|
||||
'You have successfully authenticated with Zoho!',
|
||||
[
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => {
|
||||
// Here you would typically:
|
||||
// 1. Store the tokens securely
|
||||
// 2. Update your app state
|
||||
// 3. Navigate to the main app
|
||||
// 4. Make API calls to Zoho services
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
// Handle Zoho authentication error
|
||||
const handleZohoAuthError = (error: string) => {
|
||||
console.error('Zoho Auth Error:', error);
|
||||
setShowZohoAuth(false);
|
||||
|
||||
Alert.alert(
|
||||
'Authentication Failed',
|
||||
`Failed to authenticate with Zoho: ${error}`,
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
};
|
||||
|
||||
// Handle closing the Zoho auth modal
|
||||
const handleCloseZohoAuth = () => {
|
||||
setShowZohoAuth(false);
|
||||
};
|
||||
|
||||
// Handle Zoho login button press
|
||||
const handleZohoLogin = () => {
|
||||
setShowZohoAuth(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={[styles.content, { backgroundColor: colors.surface, ...shadows.medium }]}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<Icon name="account-circle" size={32} color={colors.primary} />
|
||||
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
|
||||
Zoho Integration
|
||||
</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.textLight, fontFamily: fonts.regular }]}>
|
||||
Connect your Zoho account to access projects and data
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Auth Status */}
|
||||
{authData ? (
|
||||
<View style={[styles.statusContainer, { backgroundColor: colors.success + '20' }]}>
|
||||
<Icon name="check-circle" size={24} color={colors.success} />
|
||||
<Text style={[styles.statusText, { color: colors.success, fontFamily: fonts.medium }]}>
|
||||
Connected to Zoho
|
||||
</Text>
|
||||
<Text style={[styles.statusSubtext, { color: colors.textLight, fontFamily: fonts.regular }]}>
|
||||
Access Token: {authData.accessToken.substring(0, 20)}...
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[styles.statusContainer, { backgroundColor: colors.warning + '20' }]}>
|
||||
<Icon name="warning" size={24} color={colors.warning} />
|
||||
<Text style={[styles.statusText, { color: colors.warning, fontFamily: fonts.medium }]}>
|
||||
Not Connected
|
||||
</Text>
|
||||
<Text style={[styles.statusSubtext, { color: colors.textLight, fontFamily: fonts.regular }]}>
|
||||
Connect your Zoho account to get started
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Zoho Login Button */}
|
||||
<TouchableOpacity
|
||||
style={[styles.zohoButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleZohoLogin}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Icon name="login" size={20} color={colors.surface} />
|
||||
<Text style={[styles.zohoButtonText, { color: colors.surface, fontFamily: fonts.medium }]}>
|
||||
{authData ? 'Reconnect to Zoho' : 'Connect to Zoho'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Instructions */}
|
||||
<View style={styles.instructionsContainer}>
|
||||
<Text style={[styles.instructionsTitle, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||
What you'll get access to:
|
||||
</Text>
|
||||
<View style={styles.featureList}>
|
||||
<View style={styles.featureItem}>
|
||||
<Icon name="check" size={16} color={colors.success} />
|
||||
<Text style={[styles.featureText, { color: colors.textLight, fontFamily: fonts.regular }]}>
|
||||
Zoho Projects data and analytics
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.featureItem}>
|
||||
<Icon name="check" size={16} color={colors.success} />
|
||||
<Text style={[styles.featureText, { color: colors.textLight, fontFamily: fonts.regular }]}>
|
||||
Task and project management
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.featureItem}>
|
||||
<Icon name="check" size={16} color={colors.success} />
|
||||
<Text style={[styles.featureText, { color: colors.textLight, fontFamily: fonts.regular }]}>
|
||||
Time tracking and reporting
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.featureItem}>
|
||||
<Icon name="check" size={16} color={colors.success} />
|
||||
<Text style={[styles.featureText, { color: colors.textLight, fontFamily: fonts.regular }]}>
|
||||
Team collaboration insights
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Zoho Auth Modal */}
|
||||
<Modal
|
||||
visible={showZohoAuth}
|
||||
animationType="slide"
|
||||
presentationStyle="fullScreen"
|
||||
onRequestClose={handleCloseZohoAuth}
|
||||
>
|
||||
<ZohoAuth
|
||||
onAuthSuccess={handleZohoAuthSuccess}
|
||||
onAuthError={handleZohoAuthError}
|
||||
onClose={handleCloseZohoAuth}
|
||||
/>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
content: {
|
||||
borderRadius: 16,
|
||||
padding: 24,
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
marginTop: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
statusContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
marginBottom: 24,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 16,
|
||||
marginLeft: 8,
|
||||
flex: 1,
|
||||
},
|
||||
statusSubtext: {
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
zohoButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 16,
|
||||
borderRadius: 12,
|
||||
marginBottom: 24,
|
||||
},
|
||||
zohoButtonText: {
|
||||
fontSize: 16,
|
||||
marginLeft: 8,
|
||||
},
|
||||
instructionsContainer: {
|
||||
marginTop: 8,
|
||||
},
|
||||
instructionsTitle: {
|
||||
fontSize: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
featureList: {
|
||||
gap: 8,
|
||||
},
|
||||
featureItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
featureText: {
|
||||
fontSize: 14,
|
||||
marginLeft: 8,
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default ZohoAuthExample;
|
||||
@ -73,9 +73,7 @@ const AppTabs = () => {
|
||||
};
|
||||
|
||||
const AppNavigator = () => (
|
||||
<NavigationContainer>
|
||||
<AppTabs />
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
export default AppNavigator;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user