login flow integrated
This commit is contained in:
parent
70b9198aa4
commit
0df78919f2
2
App.tsx
2
App.tsx
@ -20,7 +20,7 @@ import IntegrationsNavigator from '@/modules/integrations/navigation/Integration
|
|||||||
import { StatusBar } from 'react-native';
|
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.isAuthenticated));
|
||||||
const selectedService = useSelector((s: RootState) => s.integrations.selectedService);
|
const selectedService = useSelector((s: RootState) => s.integrations.selectedService);
|
||||||
const linking = {
|
const linking = {
|
||||||
prefixes: ['centralizedreportingsystem://', 'https://centralizedreportingsystem.com'],
|
prefixes: ['centralizedreportingsystem://', 'https://centralizedreportingsystem.com'],
|
||||||
|
|||||||
@ -6,33 +6,99 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Pressable,
|
Pressable,
|
||||||
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import GradientBackground from '@/shared/components/layout/GradientBackground';
|
import GradientBackground from '@/shared/components/layout/GradientBackground';
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { login } from '@/modules/auth/store/authSlice';
|
import { login, clearError } from '@/modules/auth/store/authSlice';
|
||||||
import type { RootState } from '@/store/store';
|
import type { RootState, AppDispatch } from '@/store/store';
|
||||||
import { useTheme } from '@/shared/styles/useTheme';
|
import { useTheme } from '@/shared/styles/useTheme';
|
||||||
|
import { validateLoginForm } from '@/shared/utils/validation';
|
||||||
|
|
||||||
const LoginScreen: React.FC = () => {
|
const LoginScreen: React.FC = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const { colors, fonts } = useTheme();
|
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 [email, setEmail] = React.useState('');
|
||||||
const [password, setPassword] = React.useState('');
|
const [password, setPassword] = React.useState('');
|
||||||
const [showPassword, setShowPassword] = React.useState(false);
|
const [showPassword, setShowPassword] = React.useState(false);
|
||||||
const [rememberMe, setRememberMe] = React.useState(false);
|
const [rememberMe, setRememberMe] = React.useState(false);
|
||||||
const [focused, setFocused] = React.useState<null | 'email' | 'password'>(null);
|
const [focused, setFocused] = React.useState<null | 'email' | 'password'>(null);
|
||||||
|
const [validationErrors, setValidationErrors] = React.useState<{ email?: string; password?: string }>({});
|
||||||
|
|
||||||
const emailRef = React.useRef<TextInput>(null);
|
const emailRef = React.useRef<TextInput>(null);
|
||||||
const passwordRef = React.useRef<TextInput>(null);
|
const passwordRef = React.useRef<TextInput>(null);
|
||||||
|
|
||||||
const handleLogin = () => {
|
// Clear validation errors when user starts typing
|
||||||
// @ts-ignore
|
const handleEmailChange = (text: string) => {
|
||||||
dispatch(login({ email, password }));
|
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 (
|
return (
|
||||||
<GradientBackground preset="warm" style={styles.gradient}>
|
<GradientBackground preset="warm" style={styles.gradient}>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@ -47,72 +113,112 @@ const LoginScreen: React.FC = () => {
|
|||||||
<Text style={[styles.subtitle, { color: colors.textLight, fontFamily: fonts.regular }]}>Enter your email and password to log in</Text>
|
<Text style={[styles.subtitle, { color: colors.textLight, fontFamily: fonts.regular }]}>Enter your email and password to log in</Text>
|
||||||
|
|
||||||
{/* Email input */}
|
{/* Email input */}
|
||||||
<Pressable
|
<View>
|
||||||
style={[
|
<Pressable
|
||||||
styles.inputWrapper,
|
style={[
|
||||||
{
|
styles.inputWrapper,
|
||||||
borderColor: focused === 'email' ? colors.primary : colors.border,
|
{
|
||||||
backgroundColor: colors.surface,
|
borderColor: validationErrors.email
|
||||||
},
|
? colors.error
|
||||||
]}
|
: focused === 'email'
|
||||||
onPress={() => emailRef.current?.focus()}
|
? colors.primary
|
||||||
>
|
: colors.border,
|
||||||
<Icon name="email-outline" size={20} color={focused === 'email' ? colors.primary : colors.textLight} />
|
backgroundColor: colors.surface,
|
||||||
<TextInput
|
},
|
||||||
ref={emailRef}
|
]}
|
||||||
placeholder="Email"
|
onPress={() => emailRef.current?.focus()}
|
||||||
placeholderTextColor={colors.textLight}
|
>
|
||||||
autoCapitalize="none"
|
<Icon
|
||||||
autoCorrect={false}
|
name="email-outline"
|
||||||
autoComplete="email"
|
size={20}
|
||||||
keyboardType="email-address"
|
color={validationErrors.email
|
||||||
returnKeyType="next"
|
? colors.error
|
||||||
value={email}
|
: focused === 'email'
|
||||||
onFocus={() => setFocused('email')}
|
? colors.primary
|
||||||
onBlur={() => setFocused(null)}
|
: colors.textLight
|
||||||
onChangeText={setEmail}
|
}
|
||||||
onSubmitEditing={() => passwordRef.current?.focus()}
|
/>
|
||||||
style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]}
|
<TextInput
|
||||||
/>
|
ref={emailRef}
|
||||||
</Pressable>
|
placeholder="Email"
|
||||||
|
placeholderTextColor={colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
autoComplete="email"
|
||||||
|
keyboardType="email-address"
|
||||||
|
returnKeyType="next"
|
||||||
|
value={email}
|
||||||
|
onFocus={() => setFocused('email')}
|
||||||
|
onBlur={() => setFocused(null)}
|
||||||
|
onChangeText={handleEmailChange}
|
||||||
|
onSubmitEditing={() => passwordRef.current?.focus()}
|
||||||
|
style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
{validationErrors.email && (
|
||||||
|
<Text style={[styles.errorText, { color: colors.error, fontFamily: fonts.regular }]}>
|
||||||
|
{validationErrors.email}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Password input */}
|
{/* Password input */}
|
||||||
<Pressable
|
<View>
|
||||||
style={[
|
<Pressable
|
||||||
styles.inputWrapper,
|
style={[
|
||||||
{
|
styles.inputWrapper,
|
||||||
borderColor: focused === 'password' ? colors.primary : colors.border,
|
{
|
||||||
backgroundColor: colors.surface,
|
borderColor: validationErrors.password
|
||||||
},
|
? colors.error
|
||||||
]}
|
: focused === 'password'
|
||||||
onPress={() => passwordRef.current?.focus()}
|
? colors.primary
|
||||||
>
|
: colors.border,
|
||||||
<Icon name="lock-outline" size={20} color={focused === 'password' ? colors.primary : colors.textLight} />
|
backgroundColor: colors.surface,
|
||||||
<TextInput
|
},
|
||||||
ref={passwordRef}
|
]}
|
||||||
placeholder="Password"
|
onPress={() => passwordRef.current?.focus()}
|
||||||
placeholderTextColor={colors.textLight}
|
|
||||||
secureTextEntry={!showPassword}
|
|
||||||
textContentType="password"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect={false}
|
|
||||||
autoCapitalize="none"
|
|
||||||
value={password}
|
|
||||||
onFocus={() => setFocused('password')}
|
|
||||||
onBlur={() => setFocused(null)}
|
|
||||||
onChangeText={setPassword}
|
|
||||||
style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]}
|
|
||||||
/>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.iconButton}
|
|
||||||
onPress={() => setShowPassword(v => !v)}
|
|
||||||
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={showPassword ? 'Hide password' : 'Show password'}
|
|
||||||
>
|
>
|
||||||
<Icon name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={22} color={colors.textLight} />
|
<Icon
|
||||||
</TouchableOpacity>
|
name="lock-outline"
|
||||||
</Pressable>
|
size={20}
|
||||||
|
color={validationErrors.password
|
||||||
|
? colors.error
|
||||||
|
: focused === 'password'
|
||||||
|
? colors.primary
|
||||||
|
: colors.textLight
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
ref={passwordRef}
|
||||||
|
placeholder="Password"
|
||||||
|
placeholderTextColor={colors.textLight}
|
||||||
|
secureTextEntry={!showPassword}
|
||||||
|
textContentType="password"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect={false}
|
||||||
|
autoCapitalize="none"
|
||||||
|
value={password}
|
||||||
|
onFocus={() => setFocused('password')}
|
||||||
|
onBlur={() => setFocused(null)}
|
||||||
|
onChangeText={handlePasswordChange}
|
||||||
|
style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.iconButton}
|
||||||
|
onPress={() => setShowPassword(v => !v)}
|
||||||
|
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={showPassword ? 'Hide password' : 'Show password'}
|
||||||
|
>
|
||||||
|
<Icon name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={22} color={colors.textLight} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Pressable>
|
||||||
|
{validationErrors.password && (
|
||||||
|
<Text style={[styles.errorText, { color: colors.error, fontFamily: fonts.regular }]}>
|
||||||
|
{validationErrors.password}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Row: Remember me + Forgot password */}
|
{/* Row: Remember me + Forgot password */}
|
||||||
<View style={styles.rowBetween}>
|
<View style={styles.rowBetween}>
|
||||||
@ -127,9 +233,24 @@ const LoginScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Login button */}
|
{/* Login button */}
|
||||||
<TouchableOpacity activeOpacity={0.9} onPress={handleLogin} disabled={loading} style={{ marginTop: 12 }}>
|
<TouchableOpacity
|
||||||
<LinearGradient colors={["#3AA0FF", "#2D6BFF"]} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }} style={styles.loginButton}>
|
activeOpacity={0.9}
|
||||||
<Text style={[styles.loginButtonText, { fontFamily: fonts.bold }]}>{loading ? 'Logging in...' : 'Log In'}</Text>
|
onPress={handleLogin}
|
||||||
|
disabled={loading || !email.trim() || !password.trim()}
|
||||||
|
style={{ marginTop: 12 }}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={loading || !email.trim() || !password.trim()
|
||||||
|
? [colors.border, colors.border]
|
||||||
|
: ["#3AA0FF", "#2D6BFF"]
|
||||||
|
}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 0 }}
|
||||||
|
style={styles.loginButton}
|
||||||
|
>
|
||||||
|
<Text style={[styles.loginButtonText, { fontFamily: fonts.bold }]}>
|
||||||
|
{loading ? 'Logging in...' : 'Log In'}
|
||||||
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@ -161,8 +282,15 @@ const LoginScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Error */}
|
{/* API Error */}
|
||||||
{!!error && <Text style={[styles.errorText, { fontFamily: fonts.regular }]}>{error}</Text>}
|
{!!error && (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Icon name="alert-circle-outline" size={16} color={colors.error} />
|
||||||
|
<Text style={[styles.errorText, { color: colors.error, fontFamily: fonts.regular }]}>
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</GradientBackground>
|
</GradientBackground>
|
||||||
@ -287,10 +415,17 @@ const styles = StyleSheet.create({
|
|||||||
signupText: {
|
signupText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
errorText: {
|
errorContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
color: '#EF4444',
|
paddingHorizontal: 8,
|
||||||
textAlign: 'center',
|
},
|
||||||
|
errorText: {
|
||||||
|
marginLeft: 6,
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'left',
|
||||||
|
flex: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,31 +1,78 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
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 {
|
export interface AuthUser {
|
||||||
id: string;
|
id: number;
|
||||||
name: string;
|
uuid: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
displayName: string;
|
||||||
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthState {
|
export interface AuthState {
|
||||||
user: AuthUser | null;
|
user: AuthUser | null;
|
||||||
token: string | null;
|
accessToken: string | null;
|
||||||
|
refreshToken: string | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
user: null,
|
user: null,
|
||||||
token: null,
|
accessToken: null,
|
||||||
|
refreshToken: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
isAuthenticated: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const login = createAsyncThunk(
|
export const login = createAsyncThunk(
|
||||||
'auth/login',
|
'auth/login',
|
||||||
async (payload: { email: string; password: string }) => {
|
async (payload: { email: string; password: string }, { rejectWithValue }) => {
|
||||||
// TODO: integrate real API
|
try {
|
||||||
await new Promise(r => setTimeout(r, 300));
|
const response = await authAPI.login(payload.email, payload.password);
|
||||||
return { token: 'mock-token', user: { id: '1', name: 'User', email: payload.email } as AuthUser };
|
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: {
|
reducers: {
|
||||||
logout: state => {
|
logout: state => {
|
||||||
state.user = null;
|
state.user = null;
|
||||||
state.token = null;
|
state.accessToken = null;
|
||||||
|
state.refreshToken = null;
|
||||||
|
state.error = null;
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
},
|
||||||
|
clearError: state => {
|
||||||
state.error = null;
|
state.error = null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -45,19 +97,23 @@ const authSlice = createSlice({
|
|||||||
state.loading = true;
|
state.loading = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(login.fulfilled, (state, action: PayloadAction<{ token: string; user: AuthUser }>) => {
|
.addCase(login.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.token = action.payload.token;
|
state.accessToken = action.payload.accessToken;
|
||||||
|
state.refreshToken = action.payload.refreshToken;
|
||||||
state.user = action.payload.user;
|
state.user = action.payload.user;
|
||||||
|
state.isAuthenticated = true;
|
||||||
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(login.rejected, (state, action) => {
|
.addCase(login.rejected, (state, action) => {
|
||||||
state.loading = false;
|
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;
|
export default authSlice;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,9 @@ import { WebView } from 'react-native-webview';
|
|||||||
import Icon from 'react-native-vector-icons/MaterialIcons';
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { useTheme } from '@/shared/styles/useTheme';
|
import { useTheme } from '@/shared/styles/useTheme';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { manageToken } from '../services/integrationAPI';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { RootState } from '@/store/store';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
type ServiceKey = 'zohoProjects' | 'zohoCRM' | 'zohoBooks' | 'zohoPeople';
|
type ServiceKey = 'zohoProjects' | 'zohoCRM' | 'zohoBooks' | 'zohoPeople';
|
||||||
@ -63,6 +66,8 @@ const getScopeForService = (_serviceKey?: ServiceKey): string => {
|
|||||||
'ZohoBooks.FullAccess.READ',
|
'ZohoBooks.FullAccess.READ',
|
||||||
// Zoho People
|
// Zoho People
|
||||||
'ZohoPeople.employee.READ',
|
'ZohoPeople.employee.READ',
|
||||||
|
//can read the user info
|
||||||
|
'aaaserver.profile.READ'
|
||||||
].join(',');
|
].join(',');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -143,7 +148,7 @@ const ZohoAuth: React.FC<ZohoAuthProps> = ({
|
|||||||
const { colors, spacing, fonts, shadows } = useTheme();
|
const { colors, spacing, fonts, shadows } = useTheme();
|
||||||
const webViewRef = useRef<WebView>(null);
|
const webViewRef = useRef<WebView>(null);
|
||||||
const navigation = useNavigation<any>();
|
const navigation = useNavigation<any>();
|
||||||
|
const { user,accessToken } = useSelector((state: RootState) => state.auth);
|
||||||
const currentScope = useMemo(() => getScopeForService(serviceKey), [serviceKey]);
|
const currentScope = useMemo(() => getScopeForService(serviceKey), [serviceKey]);
|
||||||
|
|
||||||
const [state, setState] = useState<ZohoAuthState>({
|
const [state, setState] = useState<ZohoAuthState>({
|
||||||
@ -153,9 +158,11 @@ const ZohoAuth: React.FC<ZohoAuthProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Backend exchange mode: only log and return the authorization code to the caller
|
// 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] Authorization code received:', authCode);
|
||||||
console.log('[ZohoAuth] Send this code to your backend to exchange for tokens.');
|
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
|
// Return the code via onAuthSuccess using the existing shape
|
||||||
onAuthSuccess?.({
|
onAuthSuccess?.({
|
||||||
accessToken: authCode, // This is the AUTHORIZATION CODE, not an access token
|
accessToken: authCode, // This is the AUTHORIZATION CODE, not an access token
|
||||||
@ -194,18 +201,18 @@ const ZohoAuth: React.FC<ZohoAuthProps> = ({
|
|||||||
navigation.navigate('Dashboard' as never, params as never);
|
navigation.navigate('Dashboard' as never, params as never);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (lowerUrl.includes('profile')) {
|
// if (lowerUrl.includes('profile')) {
|
||||||
// Close auth view if provided and navigate to Profile tab
|
// // Close auth view if provided and navigate to Profile tab
|
||||||
const params = getAllQueryParamsFromUrl(url || '');
|
// const params = getAllQueryParamsFromUrl(url || '');
|
||||||
// Prefer `userId` param when present and numeric
|
// // Prefer `userId` param when present and numeric
|
||||||
if (typeof params.userId === 'string' && /^-?\d+$/.test(params.userId)) {
|
// if (typeof params.userId === 'string' && /^-?\d+$/.test(params.userId)) {
|
||||||
params.userId = Number(params.userId);
|
// params.userId = Number(params.userId);
|
||||||
}
|
// }
|
||||||
onClose?.();
|
// onClose?.();
|
||||||
// Route into Profile stack's default screen with params
|
// // Route into Profile stack's default screen with params
|
||||||
navigation.navigate('Profile' as never, { screen: 'Profile', params } as never);
|
// navigation.navigate('Profile' as never, { screen: 'Profile', params } as never);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Fallback: if redirect URI without code or with error
|
// Fallback: if redirect URI without code or with error
|
||||||
if (isRedirectUri(url) && !loading) {
|
if (isRedirectUri(url) && !loading) {
|
||||||
|
|||||||
7
src/modules/integrations/services/integrationAPI.ts
Normal file
7
src/modules/integrations/services/integrationAPI.ts
Normal file
@ -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}`}}),
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { create } from 'apisauce';
|
import { create } from 'apisauce';
|
||||||
|
|
||||||
const http = create({
|
const http = create({
|
||||||
baseURL: 'https://api.example.com',
|
baseURL: 'http://192.168.1.16:4000',
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
export const API_ENDPOINTS = {
|
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',
|
HR_METRICS: '/hr/metrics',
|
||||||
ZOHO_PROJECTS: '/zoho/projects',
|
ZOHO_PROJECTS: '/zoho/projects',
|
||||||
PROFILE: '/profile',
|
PROFILE: '/profile',
|
||||||
|
|||||||
44
src/shared/utils/validation.ts
Normal file
44
src/shared/utils/validation.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user