295 lines
10 KiB
TypeScript
295 lines
10 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import {
|
|
Text,
|
|
View,
|
|
TouchableOpacity,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
ScrollView,
|
|
ImageBackground,
|
|
ActivityIndicator,
|
|
StatusBar,
|
|
} from 'react-native';
|
|
import { useNavigation } from '@react-navigation/native';
|
|
import Icon from 'react-native-vector-icons/Ionicons';
|
|
import { getStyles } from './login.styles';
|
|
import { useTheme } from '@theme';
|
|
import { useAppDispatch, useAppSelector, RootState, login } from '@store';
|
|
import { LoginRequest } from '@interfaces';
|
|
import { NotificationService } from '@services';
|
|
import { backGroundImage, TENANCY_OPTIONS } from '@utils';
|
|
import { FormInput, FormPicker } from '@components';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import DeviceInfo from 'react-native-device-info';
|
|
|
|
export const LoginScreen = () => {
|
|
const dispatch = useAppDispatch();
|
|
const navigation = useNavigation<any>();
|
|
const { theme: colors, isDark } = useTheme();
|
|
const styles = getStyles(colors, isDark);
|
|
|
|
const [tenancy, setTenancy] = useState('');
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [focusedField, setFocusedField] = useState<string | null>(null);
|
|
const scrollViewRef = useRef<ScrollView>(null);
|
|
const [localError, setLocalError] = useState('');
|
|
// const [deviceToken, setDeviceToken] = useState<string | null>(null);
|
|
|
|
const { loginLoading, loginError, loginSuccess, token, user_data } =
|
|
useAppSelector((state: RootState) => state.auth);
|
|
|
|
const handleLogin = async () => {
|
|
setLocalError('');
|
|
if (!tenancy.trim()) {
|
|
setLocalError('Tenancy name is required');
|
|
return;
|
|
}
|
|
if (!email.trim()) {
|
|
setLocalError('Email address is required');
|
|
return;
|
|
}
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(email.trim())) {
|
|
setLocalError('Please enter a valid email address');
|
|
return;
|
|
}
|
|
if (!password.trim()) {
|
|
setLocalError('Password is required');
|
|
return;
|
|
}
|
|
|
|
const fcmToken = await NotificationService.getFCMToken();
|
|
const deviceId = await DeviceInfo.getUniqueId();
|
|
console.log('fcm_token-', fcmToken);
|
|
console.log('device_id-', deviceId);
|
|
|
|
const payload: LoginRequest = {
|
|
email,
|
|
password,
|
|
fcm_token: fcmToken ?? '',
|
|
device_id: deviceId ?? '',
|
|
};
|
|
console.log('payload', payload)
|
|
// Build the full tenancy base URL and persist it in AsyncStorage
|
|
const cleanTenancy = tenancy.trim().replace(/^(https?:\/\/)?/, '');
|
|
const fullTenancy = `https://${cleanTenancy}`;
|
|
console.log('baseUrl set to-', fullTenancy);
|
|
|
|
try {
|
|
await AsyncStorage.setItem('base_url', fullTenancy);
|
|
await AsyncStorage.setItem('tenancy_name', cleanTenancy);
|
|
await AsyncStorage.setItem('email', email);
|
|
await AsyncStorage.setItem('password', password);
|
|
} catch (e) {
|
|
console.error('Failed to save login credentials to AsyncStorage', e);
|
|
}
|
|
|
|
await dispatch(login(payload));
|
|
};
|
|
|
|
useEffect(() => {
|
|
const loadCredentials = async () => {
|
|
try {
|
|
const savedBaseUrl = await AsyncStorage.getItem('base_url');
|
|
const savedEmail = await AsyncStorage.getItem('email');
|
|
const savedPassword = await AsyncStorage.getItem('password');
|
|
|
|
if (savedBaseUrl) {
|
|
const matched = TENANCY_OPTIONS.find(
|
|
opt =>
|
|
opt.value.trim().replace(/\/+$/, '').toLowerCase() ===
|
|
savedBaseUrl.trim().replace(/\/+$/, '').toLowerCase(),
|
|
);
|
|
setTenancy(matched ? matched.value : savedBaseUrl);
|
|
} else if (TENANCY_OPTIONS.length > 0) {
|
|
setTenancy(TENANCY_OPTIONS[0].value);
|
|
}
|
|
if (savedEmail) setEmail(savedEmail);
|
|
if (savedPassword) setPassword(savedPassword);
|
|
} catch (e) {
|
|
console.error('Failed to load login credentials from AsyncStorage', e);
|
|
}
|
|
};
|
|
loadCredentials();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (
|
|
tenancy.trim() &&
|
|
email.trim() &&
|
|
emailRegex.test(email.trim()) &&
|
|
password.trim()
|
|
) {
|
|
setLocalError('');
|
|
}
|
|
}, [tenancy, email, password]);
|
|
|
|
useEffect(() => {
|
|
if (loginSuccess && token && user_data) {
|
|
// Send FCM token to server immediately after login
|
|
// if (deviceToken && user_data.staffid) {
|
|
// dispatch(sendFcmToken({ id: user_data.staffid, fcm_token: deviceToken }));
|
|
// }
|
|
navigation.reset({
|
|
index: 0,
|
|
routes: [{ name: 'DrawerStack' }],
|
|
});
|
|
}
|
|
}, [loginSuccess, token, user_data, navigation]);
|
|
|
|
return (
|
|
<ImageBackground
|
|
source={backGroundImage}
|
|
style={styles.backgroundImage}
|
|
resizeMode="cover"
|
|
>
|
|
<StatusBar
|
|
barStyle={isDark ? 'light-content' : 'dark-content'}
|
|
backgroundColor="transparent"
|
|
translucent
|
|
/>
|
|
<View style={styles.overlay}>
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
style={styles.container}
|
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
|
|
>
|
|
<ScrollView
|
|
ref={scrollViewRef}
|
|
contentContainerStyle={styles.scrollContainer}
|
|
keyboardShouldPersistTaps="handled"
|
|
showsVerticalScrollIndicator={false}
|
|
bounces={false}
|
|
keyboardDismissMode="none"
|
|
>
|
|
{/* Top Area Spacer displaying background illustration */}
|
|
<View style={styles.topSpacer} />
|
|
|
|
{/* Bottom Sheet Form Card */}
|
|
<View style={styles.bottomSheetCard}>
|
|
<View style={styles.sheetHandle} />
|
|
|
|
<View style={styles.cardHeader}>
|
|
<Text style={styles.welcomeTitle}>Sign In</Text>
|
|
<Text style={styles.welcomeSubtitle}>
|
|
Enter your credentials to continue
|
|
</Text>
|
|
</View>
|
|
|
|
{localError || loginError ? (
|
|
<View style={styles.errorBanner}>
|
|
<Icon name="alert-circle-outline" size={20} color="#EF4444" />
|
|
<Text style={styles.errorText}>{localError || loginError}</Text>
|
|
</View>
|
|
) : null}
|
|
|
|
{/* Tenancy Environment Field */}
|
|
<FormPicker
|
|
label="Tenancy Name"
|
|
value={tenancy}
|
|
onValueChange={(val) => setTenancy(val)}
|
|
options={TENANCY_OPTIONS}
|
|
placeholder="Select Tenancy Environment"
|
|
/>
|
|
|
|
{/* Email Field */}
|
|
<FormInput
|
|
label="Email Address"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
placeholder="name@company.com"
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
containerStyle={styles.fieldContainer}
|
|
inputContainerStyle={[
|
|
styles.inputWrapper,
|
|
focusedField === 'email' && styles.inputWrapperFocused,
|
|
]}
|
|
onFocus={() => {
|
|
setFocusedField('email');
|
|
setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 100);
|
|
}}
|
|
onBlur={() => setFocusedField(null)}
|
|
leftIcon={
|
|
<Icon
|
|
name="mail-outline"
|
|
size={20}
|
|
color={
|
|
focusedField === 'email'
|
|
? colors.icon
|
|
: colors.textSecondary
|
|
}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
{/* Password Field */}
|
|
<FormInput
|
|
label="Password"
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
placeholder="••••••••"
|
|
secureTextEntry={!showPassword}
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
containerStyle={styles.fieldContainer}
|
|
inputContainerStyle={[
|
|
styles.inputWrapper,
|
|
focusedField === 'password' && styles.inputWrapperFocused,
|
|
]}
|
|
onFocus={() => {
|
|
setFocusedField('password');
|
|
setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 100);
|
|
}}
|
|
onBlur={() => setFocusedField(null)}
|
|
leftIcon={
|
|
<Icon
|
|
name="lock-closed-outline"
|
|
size={20}
|
|
color={
|
|
focusedField === 'password'
|
|
? colors.icon
|
|
: colors.textSecondary
|
|
}
|
|
/>
|
|
}
|
|
rightIcon={
|
|
<TouchableOpacity
|
|
onPress={() => setShowPassword(!showPassword)}
|
|
style={styles.eyeIconButton}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Icon
|
|
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
|
|
size={20}
|
|
color={colors.textMuted}
|
|
/>
|
|
</TouchableOpacity>
|
|
}
|
|
/>
|
|
|
|
{/* Sign In Button */}
|
|
<TouchableOpacity
|
|
style={[styles.button, loginLoading && styles.buttonDisabled]}
|
|
activeOpacity={0.85}
|
|
onPress={handleLogin}
|
|
disabled={loginLoading}
|
|
>
|
|
{loginLoading ? (
|
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
) : (
|
|
<Text style={styles.buttonText}>Sign In</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
</View>
|
|
</ImageBackground>
|
|
);
|
|
};
|