diff --git a/App.tsx b/App.tsx index 579b91f..3ba1d32 100644 --- a/App.tsx +++ b/App.tsx @@ -20,7 +20,7 @@ import IntegrationsNavigator from '@/modules/integrations/navigation/Integration import { StatusBar } from 'react-native'; function AppContent(): React.JSX.Element { - const isAuthenticated = useSelector((s: RootState) => Boolean(s.auth.token)); + const isAuthenticated = useSelector((s: RootState) => Boolean(s.auth.isAuthenticated)); const selectedService = useSelector((s: RootState) => s.integrations.selectedService); const linking = { prefixes: ['centralizedreportingsystem://', 'https://centralizedreportingsystem.com'], diff --git a/src/modules/auth/screens/LoginScreen.tsx b/src/modules/auth/screens/LoginScreen.tsx index f263fd4..92e9309 100644 --- a/src/modules/auth/screens/LoginScreen.tsx +++ b/src/modules/auth/screens/LoginScreen.tsx @@ -6,33 +6,99 @@ import { TextInput, TouchableOpacity, Pressable, + Alert, } from 'react-native'; import GradientBackground from '@/shared/components/layout/GradientBackground'; import LinearGradient from 'react-native-linear-gradient'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import { useDispatch, useSelector } from 'react-redux'; -import { login } from '@/modules/auth/store/authSlice'; -import type { RootState } from '@/store/store'; +import { login, clearError } from '@/modules/auth/store/authSlice'; +import type { RootState, AppDispatch } from '@/store/store'; import { useTheme } from '@/shared/styles/useTheme'; +import { validateLoginForm } from '@/shared/utils/validation'; const LoginScreen: React.FC = () => { - const dispatch = useDispatch(); + const dispatch = useDispatch(); const { colors, fonts } = useTheme(); - const { loading, error } = useSelector((s: RootState) => s.auth); + const { loading, error, isAuthenticated } = useSelector((s: RootState) => s.auth); const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState(''); const [showPassword, setShowPassword] = React.useState(false); const [rememberMe, setRememberMe] = React.useState(false); const [focused, setFocused] = React.useState(null); + const [validationErrors, setValidationErrors] = React.useState<{ email?: string; password?: string }>({}); const emailRef = React.useRef(null); const passwordRef = React.useRef(null); - const handleLogin = () => { - // @ts-ignore - dispatch(login({ email, password })); + // Clear validation errors when user starts typing + const handleEmailChange = (text: string) => { + setEmail(text); + if (validationErrors.email) { + setValidationErrors(prev => ({ ...prev, email: undefined })); + } + // Clear API error when user starts typing + if (error) { + dispatch(clearError()); + } }; + + const handlePasswordChange = (text: string) => { + setPassword(text); + if (validationErrors.password) { + setValidationErrors(prev => ({ ...prev, password: undefined })); + } + // Clear API error when user starts typing + if (error) { + dispatch(clearError()); + } + }; + + const handleLogin = async () => { + // Clear previous validation errors + setValidationErrors({}); + + // Validate form inputs + const validation = validateLoginForm(email, password); + + if (!validation.isValid) { + setValidationErrors(validation.errors); + return; + } + + try { + // Dispatch login action + const result = await dispatch(login({ email, password })); + + // Check if login was successful + if (login.fulfilled.match(result)) { + // Login successful - navigation will be handled by the app navigator + // based on isAuthenticated state + Alert.alert('Success', 'Login successful!', [ + { text: 'OK', style: 'default' } + ]); + } + } catch (err) { + // Error handling is done in the slice + console.error('Login error:', err); + } + }; + + // Clear error when component mounts or when user starts typing + React.useEffect(() => { + if (error) { + dispatch(clearError()); + } + }, [dispatch]); + + // Auto-focus email input when component mounts + React.useEffect(() => { + const timer = setTimeout(() => { + emailRef.current?.focus(); + }, 100); + return () => clearTimeout(timer); + }, []); return ( @@ -47,72 +113,112 @@ const LoginScreen: React.FC = () => { Enter your email and password to log in {/* Email input */} - emailRef.current?.focus()} - > - - setFocused('email')} - onBlur={() => setFocused(null)} - onChangeText={setEmail} - onSubmitEditing={() => passwordRef.current?.focus()} - style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]} - /> - + + emailRef.current?.focus()} + > + + setFocused('email')} + onBlur={() => setFocused(null)} + onChangeText={handleEmailChange} + onSubmitEditing={() => passwordRef.current?.focus()} + style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]} + /> + + {validationErrors.email && ( + + {validationErrors.email} + + )} + {/* Password input */} - passwordRef.current?.focus()} - > - - setFocused('password')} - onBlur={() => setFocused(null)} - onChangeText={setPassword} - style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]} - /> - setShowPassword(v => !v)} - hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }} - accessibilityRole="button" - accessibilityLabel={showPassword ? 'Hide password' : 'Show password'} + + passwordRef.current?.focus()} > - - - + + setFocused('password')} + onBlur={() => setFocused(null)} + onChangeText={handlePasswordChange} + style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]} + /> + setShowPassword(v => !v)} + hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }} + accessibilityRole="button" + accessibilityLabel={showPassword ? 'Hide password' : 'Show password'} + > + + + + {validationErrors.password && ( + + {validationErrors.password} + + )} + {/* Row: Remember me + Forgot password */} @@ -127,9 +233,24 @@ const LoginScreen: React.FC = () => { {/* Login button */} - - - {loading ? 'Logging in...' : 'Log In'} + + + + {loading ? 'Logging in...' : 'Log In'} + @@ -161,8 +282,15 @@ const LoginScreen: React.FC = () => { - {/* Error */} - {!!error && {error}} + {/* API Error */} + {!!error && ( + + + + {error} + + + )} @@ -287,10 +415,17 @@ const styles = StyleSheet.create({ signupText: { fontSize: 12, }, - errorText: { + errorContainer: { + flexDirection: 'row', + alignItems: 'center', marginTop: 12, - color: '#EF4444', - textAlign: 'center', + paddingHorizontal: 8, + }, + errorText: { + marginLeft: 6, + fontSize: 14, + textAlign: 'left', + flex: 1, }, }); diff --git a/src/modules/auth/store/authSlice.ts b/src/modules/auth/store/authSlice.ts index f3255b1..9a1b56e 100644 --- a/src/modules/auth/store/authSlice.ts +++ b/src/modules/auth/store/authSlice.ts @@ -1,31 +1,78 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { authAPI } from '../services/authAPI'; + +// API Response Types +interface LoginSuccessResponse { + status: 'success'; + message: string; + data: { + accessToken: string; + refreshToken: string; + user: AuthUser; + }; + timestamp: string; +} + +interface LoginErrorResponse { + status: 'error'; + message: string; + errorCode: string; + timestamp: string; +} + +type LoginResponse = LoginSuccessResponse | LoginErrorResponse; export interface AuthUser { - id: string; - name: string; + id: number; + uuid: string; email: string; + displayName: string; + role: string; } export interface AuthState { user: AuthUser | null; - token: string | null; + accessToken: string | null; + refreshToken: string | null; loading: boolean; error: string | null; + isAuthenticated: boolean; } const initialState: AuthState = { user: null, - token: null, + accessToken: null, + refreshToken: null, loading: false, error: null, + isAuthenticated: false, }; export const login = createAsyncThunk( 'auth/login', - async (payload: { email: string; password: string }) => { - // TODO: integrate real API - await new Promise(r => setTimeout(r, 300)); - return { token: 'mock-token', user: { id: '1', name: 'User', email: payload.email } as AuthUser }; + async (payload: { email: string; password: string }, { rejectWithValue }) => { + try { + const response = await authAPI.login(payload.email, payload.password); + console.log('login response igot',response) + const data = response.data as LoginResponse; + + if (data.status === 'success') { + return { + accessToken: data.data.accessToken, + refreshToken: data.data.refreshToken, + user: data.data.user, + }; + } else { + return rejectWithValue(data.message || 'Login failed'); + } + } catch (error: any) { + // Handle network errors or API errors + if (error.response?.data?.status === 'error') { + const errorData = error.response.data as LoginErrorResponse; + return rejectWithValue(errorData.message || 'Login failed'); + } + return rejectWithValue(error.message || 'Network error occurred'); + } }, ); @@ -35,7 +82,12 @@ const authSlice = createSlice({ reducers: { logout: state => { state.user = null; - state.token = null; + state.accessToken = null; + state.refreshToken = null; + state.error = null; + state.isAuthenticated = false; + }, + clearError: state => { state.error = null; }, }, @@ -45,19 +97,23 @@ const authSlice = createSlice({ state.loading = true; state.error = null; }) - .addCase(login.fulfilled, (state, action: PayloadAction<{ token: string; user: AuthUser }>) => { + .addCase(login.fulfilled, (state, action) => { state.loading = false; - state.token = action.payload.token; + state.accessToken = action.payload.accessToken; + state.refreshToken = action.payload.refreshToken; state.user = action.payload.user; + state.isAuthenticated = true; + state.error = null; }) .addCase(login.rejected, (state, action) => { state.loading = false; - state.error = action.error.message || 'Login failed'; + state.error = action.payload as string || 'Login failed'; + state.isAuthenticated = false; }); }, }); -export const { logout } = authSlice.actions; +export const { logout, clearError } = authSlice.actions; export default authSlice; diff --git a/src/modules/integrations/screens/ZohoAuth.tsx b/src/modules/integrations/screens/ZohoAuth.tsx index 4a7b668..446a5a5 100644 --- a/src/modules/integrations/screens/ZohoAuth.tsx +++ b/src/modules/integrations/screens/ZohoAuth.tsx @@ -12,6 +12,9 @@ import { WebView } from 'react-native-webview'; import Icon from 'react-native-vector-icons/MaterialIcons'; import { useTheme } from '@/shared/styles/useTheme'; import { useNavigation } from '@react-navigation/native'; +import { manageToken } from '../services/integrationAPI'; +import { useSelector } from 'react-redux'; +import { RootState } from '@/store/store'; // Types type ServiceKey = 'zohoProjects' | 'zohoCRM' | 'zohoBooks' | 'zohoPeople'; @@ -63,6 +66,8 @@ const getScopeForService = (_serviceKey?: ServiceKey): string => { 'ZohoBooks.FullAccess.READ', // Zoho People 'ZohoPeople.employee.READ', + //can read the user info + 'aaaserver.profile.READ' ].join(','); }; @@ -143,7 +148,7 @@ const ZohoAuth: React.FC = ({ const { colors, spacing, fonts, shadows } = useTheme(); const webViewRef = useRef(null); const navigation = useNavigation(); - + const { user,accessToken } = useSelector((state: RootState) => state.auth); const currentScope = useMemo(() => getScopeForService(serviceKey), [serviceKey]); const [state, setState] = useState({ @@ -153,9 +158,11 @@ const ZohoAuth: React.FC = ({ }); // Backend exchange mode: only log and return the authorization code to the caller - const handleAuthorizationCode = useCallback((authCode: string) => { + const handleAuthorizationCode = useCallback(async (authCode: string) => { console.log('[ZohoAuth] Authorization code received:', authCode); console.log('[ZohoAuth] Send this code to your backend to exchange for tokens.'); + const response = await manageToken.manageToken({ authorization_code: authCode, id: user?.id, service_name: 'zoho', access_token: accessToken }); + console.log('[ZohoAuth] Response from manageToken:', response); // Return the code via onAuthSuccess using the existing shape onAuthSuccess?.({ accessToken: authCode, // This is the AUTHORIZATION CODE, not an access token @@ -194,18 +201,18 @@ const ZohoAuth: React.FC = ({ 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; - } + // 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) { diff --git a/src/modules/integrations/services/integrationAPI.ts b/src/modules/integrations/services/integrationAPI.ts new file mode 100644 index 0000000..9f713e4 --- /dev/null +++ b/src/modules/integrations/services/integrationAPI.ts @@ -0,0 +1,7 @@ +import http from '@/services/http'; +import { API_ENDPOINTS } from '@/shared/constants/API_ENDPOINTS'; + +export const manageToken = { + manageToken: (data: { authorization_code: string, id: string|undefined, service_name: string|undefined ,access_token:string}) => + http.post(API_ENDPOINTS.MANAGE_TOKEN, { authorization_code:data.authorization_code,id:data.id,service_name:data.service_name },{headers:{'Authorization':`Bearer ${data.access_token}`}}), +}; \ No newline at end of file diff --git a/src/services/http.ts b/src/services/http.ts index 9055271..939e1bd 100644 --- a/src/services/http.ts +++ b/src/services/http.ts @@ -1,7 +1,7 @@ import { create } from 'apisauce'; const http = create({ - baseURL: 'https://api.example.com', + baseURL: 'http://192.168.1.16:4000', timeout: 10000, }); diff --git a/src/shared/constants/API_ENDPOINTS.ts b/src/shared/constants/API_ENDPOINTS.ts index 4fc880f..0824046 100644 --- a/src/shared/constants/API_ENDPOINTS.ts +++ b/src/shared/constants/API_ENDPOINTS.ts @@ -1,5 +1,7 @@ export const API_ENDPOINTS = { - AUTH_LOGIN: '/auth/login', + AUTH_LOGIN: '/api/v1/auth/login', + USERSIGNUP:'/api/v1/users/register', + MANAGE_TOKEN:'/api/v1/users/zoho/token ', HR_METRICS: '/hr/metrics', ZOHO_PROJECTS: '/zoho/projects', PROFILE: '/profile', diff --git a/src/shared/utils/validation.ts b/src/shared/utils/validation.ts new file mode 100644 index 0000000..aab237b --- /dev/null +++ b/src/shared/utils/validation.ts @@ -0,0 +1,44 @@ +// Validation utility functions for form inputs + +export interface ValidationResult { + isValid: boolean; + error?: string; +} + +export const validateEmail = (email: string): ValidationResult => { + if (!email.trim()) { + return { isValid: false, error: 'Email is required' }; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return { isValid: false, error: 'Please enter a valid email address' }; + } + + return { isValid: true }; +}; + +export const validatePassword = (password: string): ValidationResult => { + if (!password) { + return { isValid: false, error: 'Password is required' }; + } + + if (password.length < 6) { + return { isValid: false, error: 'Password must be at least 6 characters long' }; + } + + return { isValid: true }; +}; + +export const validateLoginForm = (email: string, password: string): { isValid: boolean; errors: { email?: string; password?: string } } => { + const emailValidation = validateEmail(email); + const passwordValidation = validatePassword(password); + + return { + isValid: emailValidation.isValid && passwordValidation.isValid, + errors: { + email: emailValidation.error, + password: passwordValidation.error, + }, + }; +};