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 {
|
function AppContent(): React.JSX.Element {
|
||||||
const isAuthenticated = useSelector((s: RootState) => Boolean(s.auth.token));
|
const isAuthenticated = useSelector((s: RootState) => Boolean(s.auth.token));
|
||||||
const selectedService = useSelector((s: RootState) => s.integrations.selectedService);
|
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 (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<PersistGate loading={<LoadingSpinner />} persistor={persistor}>
|
<PersistGate loading={<LoadingSpinner />} persistor={persistor}>
|
||||||
@ -32,11 +54,13 @@ function AppContent(): React.JSX.Element {
|
|||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
) : (
|
) : (
|
||||||
!selectedService ? (
|
!selectedService ? (
|
||||||
<NavigationContainer>
|
<NavigationContainer linking={linking} >
|
||||||
<IntegrationsNavigator/>
|
<IntegrationsNavigator/>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
) : (
|
) : (
|
||||||
|
<NavigationContainer linking={linking} >
|
||||||
<AppNavigator />
|
<AppNavigator />
|
||||||
|
</NavigationContainer>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
|
|||||||
@ -20,6 +20,27 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<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>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
15
package-lock.json
generated
15
package-lock.json
generated
@ -33,6 +33,7 @@
|
|||||||
"react-native-svg": "^15.12.1",
|
"react-native-svg": "^15.12.1",
|
||||||
"react-native-toast-message": "^2.2.1",
|
"react-native-toast-message": "^2.2.1",
|
||||||
"react-native-vector-icons": "^10.3.0",
|
"react-native-vector-icons": "^10.3.0",
|
||||||
|
"react-native-webview": "^13.16.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"redux-persist": "^6.0.0"
|
"redux-persist": "^6.0.0"
|
||||||
},
|
},
|
||||||
@ -11544,6 +11545,20 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
|
||||||
"version": "0.79.0",
|
"version": "0.79.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.0.tgz",
|
"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-svg": "^15.12.1",
|
||||||
"react-native-toast-message": "^2.2.1",
|
"react-native-toast-message": "^2.2.1",
|
||||||
"react-native-vector-icons": "^10.3.0",
|
"react-native-vector-icons": "^10.3.0",
|
||||||
|
"react-native-webview": "^13.16.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"redux-persist": "^6.0.0"
|
"redux-persist": "^6.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import type { IntegrationsStackParamList } from '@/modules/integrations/navigati
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { setSelectedService } from '@/modules/integrations/store/integrationsSlice';
|
import { setSelectedService } from '@/modules/integrations/store/integrationsSlice';
|
||||||
import type { AppDispatch } from '@/store/store';
|
import type { AppDispatch } from '@/store/store';
|
||||||
|
import ZohoAuth from './ZohoAuth';
|
||||||
|
import { Modal } from 'react-native';
|
||||||
|
|
||||||
type Route = RouteProp<IntegrationsStackParamList, 'IntegrationCategory'>;
|
type Route = RouteProp<IntegrationsStackParamList, 'IntegrationCategory'>;
|
||||||
|
|
||||||
@ -55,6 +57,8 @@ interface Props {
|
|||||||
const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
|
const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
|
||||||
const { colors, fonts } = useTheme();
|
const { colors, fonts } = useTheme();
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const [showZohoAuth, setShowZohoAuth] = React.useState(false);
|
||||||
|
const [pendingService, setPendingService] = React.useState<string | null>(null);
|
||||||
const services = servicesMap[route.params.categoryKey] ?? [];
|
const services = servicesMap[route.params.categoryKey] ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -68,7 +72,17 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.row}
|
style={styles.row}
|
||||||
activeOpacity={0.8}
|
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' }]}>
|
<View style={[styles.iconCircle, { backgroundColor: '#F1F5F9' }]}>
|
||||||
<Icon name={item.icon} size={20} color={colors.primary} />
|
<Icon name={item.icon} size={20} color={colors.primary} />
|
||||||
@ -77,6 +91,34 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
|
|||||||
</TouchableOpacity>
|
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
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 = () => (
|
const AppNavigator = () => (
|
||||||
<NavigationContainer>
|
|
||||||
<AppTabs />
|
<AppTabs />
|
||||||
</NavigationContainer>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default AppNavigator;
|
export default AppNavigator;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user