webview login for zoho implemented

This commit is contained in:
yashwin-foxy 2025-09-08 18:09:13 +05:30
parent bcaa1fbdaa
commit 70b9198aa4
9 changed files with 992 additions and 4 deletions

26
App.tsx
View File

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

View File

@ -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
View File

@ -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",

View File

@ -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"
},

View File

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

View 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

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

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

View File

@ -73,9 +73,7 @@ const AppTabs = () => {
};
const AppNavigator = () => (
<NavigationContainer>
<AppTabs />
</NavigationContainer>
);
export default AppNavigator;