Add login functionality
parent
4168f1a4b7
commit
189f8bd673
|
|
@ -1,20 +1,20 @@
|
|||
import React from "react";
|
||||
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||
import { Link, Tabs } from "expo-router";
|
||||
import { Pressable } from "react-native";
|
||||
import Colors from "@/constants/Colors";
|
||||
import { useColorScheme } from "@/components/useColorScheme";
|
||||
import { useClientOnlyValue } from "@/components/useClientOnlyValue";
|
||||
import { Tabs } from "expo-router";
|
||||
import { TAB_CONFIG } from "@/constants/config";
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
|
||||
headerShown: useClientOnlyValue(false, true),
|
||||
tabBarStyle: {
|
||||
backgroundColor: "#252A34",
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
position: "absolute",
|
||||
overflow: "hidden",
|
||||
},
|
||||
tabBarActiveTintColor: "#fff",
|
||||
tabBarInactiveTintColor: "#aaa",
|
||||
}}
|
||||
>
|
||||
{TAB_CONFIG.map(({ name, title, Icon, IconFilled }) => (
|
||||
|
|
|
|||
|
|
@ -1,37 +1,129 @@
|
|||
import { StyleSheet } from "react-native";
|
||||
import { Pressable, StyleSheet } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EditScreenInfo from "@/components/EditScreenInfo";
|
||||
import SemiCircleProgress from "../../components/home/SemiCircleProgress";
|
||||
import { Text, View } from "@/components/Themed";
|
||||
import { useNavigation } from "expo-router";
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import Profile from "../../components/home/Profile";
|
||||
import CustomerCareIcon from "../../assets/icons/customer-care.svg";
|
||||
import ServiceReminderCard from "@/components/home/ServiceReminderCard";
|
||||
import MetricCard from "@/components/home/MetricCard";
|
||||
import PaymentDueCard from "@/components/home/PaymentDueCard";
|
||||
import LocationMap from "@/components/home/LocationMap";
|
||||
import BatteryWarrantyCard from "@/components/home/BatteryWarrantyCars";
|
||||
import CustomerSupportModal from "@/components/home/CustomerSupportModal";
|
||||
|
||||
export default function TabOneScreen() {
|
||||
export default function HomeScreen() {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation();
|
||||
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerTitle: () => (
|
||||
<View style={styles.headerTitleContainer}>
|
||||
<Text style={styles.title}>Yatri - NBX 600</Text>
|
||||
<Text style={styles.subtitle}>DL253C3602</Text>
|
||||
</View>
|
||||
),
|
||||
headerRight: () => (
|
||||
<View style={styles.rightContainer}>
|
||||
<Pressable
|
||||
style={styles.iconContainer}
|
||||
onPress={() => setIsSupportModalVisible(true)}
|
||||
>
|
||||
<CustomerCareIcon />
|
||||
</Pressable>
|
||||
<Profile username="Vishal" textSize={16} boxSize={32} />
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>{t("welcome")}</Text>
|
||||
<Text style={styles.title}>{t("change-language")}</Text>
|
||||
<View
|
||||
style={styles.separator}
|
||||
lightColor="#eee"
|
||||
darkColor="rgba(255,255,255,0.1)"
|
||||
<>
|
||||
<StatusBar style="dark" />
|
||||
<View style={styles.container}>
|
||||
<ServiceReminderCard
|
||||
type="info"
|
||||
message="Service will be due in 3 days"
|
||||
subMessage="Service Reminder"
|
||||
/>
|
||||
<View style={styles.iconContainer}>
|
||||
<SemiCircleProgress progress={90} status={1} />
|
||||
</View>
|
||||
<View style={styles.metrics}>
|
||||
<MetricCard heading="SoH" value="90%" />
|
||||
<MetricCard heading="Total Distance" value="20009 km" />
|
||||
</View>
|
||||
<PaymentDueCard
|
||||
label="Payment Due"
|
||||
amount="$2,000"
|
||||
onPress={() => {}}
|
||||
/>
|
||||
{/* <LocationMap latitude={12.9716} longitude={77.5946} /> */}
|
||||
<BatteryWarrantyCard
|
||||
totalWarrantyYears={8}
|
||||
batteryPurchaseEpoch={Math.floor(
|
||||
new Date("2023-06-30").getTime() / 1000
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
<CustomerSupportModal
|
||||
visible={isSupportModalVisible}
|
||||
onClose={() => setIsSupportModalVisible(false)}
|
||||
/>
|
||||
<EditScreenInfo path="app/(tabs)/index.tsx" />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
metrics: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: "#F3F5F8",
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#F3F5F8",
|
||||
padding: 16,
|
||||
gap: 16,
|
||||
},
|
||||
iconContainer: {
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
headerTitleContainer: {
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
fontSize: 14,
|
||||
color: "#6B7280",
|
||||
fontWeight: "500",
|
||||
},
|
||||
separator: {
|
||||
marginVertical: 30,
|
||||
height: 1,
|
||||
width: "80%",
|
||||
subtitle: {
|
||||
fontSize: 18,
|
||||
color: "#111827",
|
||||
fontWeight: "700",
|
||||
},
|
||||
rightContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingRight: 16,
|
||||
gap: 8,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
badge: {
|
||||
backgroundColor: "#FEE2E2",
|
||||
borderRadius: 20,
|
||||
width: 30,
|
||||
height: 30,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
badgeText: {
|
||||
color: "#DC2626",
|
||||
fontWeight: "700",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import * as SplashScreen from "expo-splash-screen";
|
|||
import { useEffect, useState } from "react";
|
||||
import "react-native-reanimated";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import { useColorScheme } from "@/components/useColorScheme";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "@/store";
|
||||
|
||||
export { ErrorBoundary } from "expo-router";
|
||||
|
||||
|
|
@ -48,6 +48,7 @@ export default function RootLayout() {
|
|||
useEffect(() => {
|
||||
if (loaded && appIsReady) {
|
||||
SplashScreen.hideAsync();
|
||||
router.replace("/auth/login");
|
||||
}
|
||||
}, [loaded, appIsReady]);
|
||||
|
||||
|
|
@ -68,11 +69,13 @@ export default function RootLayout() {
|
|||
|
||||
function RootLayoutNav() {
|
||||
return (
|
||||
<I18nextProvider i18n={i18next}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
|
||||
</Stack>
|
||||
</I18nextProvider>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18next}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
|
||||
</Stack>
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import { Stack } from "expo-router";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { View, StyleSheet } from "react-native";
|
||||
|
||||
export default function AuthLayout() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#F3F5F8",
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
import React, { useEffect } from "react";
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
TextInput,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
Keyboard,
|
||||
KeyboardAvoidingView,
|
||||
StatusBar,
|
||||
} from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import { Formik } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { sendOTP, clearSendOTPError } from "../../store/authSlice";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { AppDispatch } from "../../store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../store";
|
||||
import { AUTH_STATUSES } from "@/constants/config";
|
||||
|
||||
// type VerifyOTPNavigationProp = StackNavigationProp<
|
||||
// RootStackParamList,
|
||||
// "VerifyOTP"
|
||||
// >;
|
||||
|
||||
// import OTPInputView from "@twotalltotems/react-native-otp-input";
|
||||
//handleblue => when input field looses focus (mark as touched)
|
||||
// handleBlur marks the field as touched, when field looses focus
|
||||
export default function WelcomeScreen() {
|
||||
const {
|
||||
status,
|
||||
otpId,
|
||||
phone: phoneNumber,
|
||||
sendOTPError,
|
||||
} = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const router = useRouter();
|
||||
|
||||
const phoneValidationSchema = Yup.object().shape({
|
||||
phone: Yup.string()
|
||||
.required("Phone number is required")
|
||||
.matches(/^\d{10}$/, "Phone number must be exactly 10 digits"),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (status === AUTH_STATUSES.SUCCESS && otpId) {
|
||||
router.push({
|
||||
pathname: "/auth/verifyOtp",
|
||||
params: { otpId, phoneNumber },
|
||||
});
|
||||
}
|
||||
}, [status, otpId, router]);
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={styles.container} behavior="padding">
|
||||
<StatusBar barStyle="dark-content" backgroundColor="#F3F5F8" />
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View style={styles.inner}>
|
||||
<Text style={styles.title}>Welcome to Driver Saathi</Text>
|
||||
|
||||
<Formik
|
||||
initialValues={{ phone: "" }}
|
||||
onSubmit={(values) => dispatch(sendOTP({ phone: values.phone }))}
|
||||
validationSchema={phoneValidationSchema}
|
||||
>
|
||||
{({
|
||||
handleChange,
|
||||
handleBlur,
|
||||
handleSubmit,
|
||||
values,
|
||||
errors,
|
||||
touched,
|
||||
}) => (
|
||||
<View style={styles.form}>
|
||||
<Text style={styles.label}>Phone Number</Text>
|
||||
<TextInput
|
||||
style={{
|
||||
...styles.input,
|
||||
color: values.phone ? "black" : "#949CAC",
|
||||
}}
|
||||
onChangeText={(text) => {
|
||||
handleChange("phone")(text);
|
||||
if (sendOTPError) {
|
||||
dispatch(clearSendOTPError());
|
||||
}
|
||||
}}
|
||||
onBlur={handleBlur("phone")}
|
||||
value={values.phone}
|
||||
keyboardType="numeric"
|
||||
placeholder="Enter your registered phone number"
|
||||
/>
|
||||
<View style={styles.errorContainer}>
|
||||
{touched.phone && errors.phone && (
|
||||
<Text style={styles.error}>{errors.phone}</Text>
|
||||
)}
|
||||
{sendOTPError && (
|
||||
<Text style={styles.error}>{sendOTPError}</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmit as unknown as () => void}
|
||||
style={{
|
||||
...styles.button,
|
||||
backgroundColor:
|
||||
values.phone.length === 10 &&
|
||||
!errors.phone &&
|
||||
status !== AUTH_STATUSES.LOADING
|
||||
? "#008761"
|
||||
: "#B0B7C5",
|
||||
}}
|
||||
disabled={
|
||||
values.phone.length !== 10 ||
|
||||
!!errors.phone ||
|
||||
status === AUTH_STATUSES.LOADING
|
||||
}
|
||||
>
|
||||
<Text style={styles.buttonText}>Send OTP</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</Formik>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
errorContainer: {
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
marginTop: 8,
|
||||
},
|
||||
error: {
|
||||
color: "#D51D10",
|
||||
fontFamily: "Inter",
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
lineHeight: 20,
|
||||
},
|
||||
inner: {
|
||||
flex: 1,
|
||||
justifyContent: "flex-start",
|
||||
position: "relative",
|
||||
},
|
||||
form: {
|
||||
height: "90%",
|
||||
position: "relative",
|
||||
},
|
||||
button: {
|
||||
height: 48,
|
||||
borderRadius: 4,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 20,
|
||||
width: "100%",
|
||||
position: "absolute",
|
||||
bottom: 50,
|
||||
},
|
||||
buttonText: {
|
||||
color: "#FCFCFC",
|
||||
fontFamily: "Inter",
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
lineHeight: 20,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
backgroundColor: "#F3F5F8",
|
||||
paddingTop: 0,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 16,
|
||||
color: "#1A1C1E",
|
||||
fontStyle: "normal",
|
||||
lineHeight: 36,
|
||||
letterSpacing: -0.56,
|
||||
width: "70%",
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
marginBottom: 8,
|
||||
color: "#717B8C",
|
||||
fontFamily: "Inter",
|
||||
fontStyle: "normal",
|
||||
fontWeight: "bold",
|
||||
lineHeight: 20,
|
||||
},
|
||||
input: {
|
||||
borderRadius: 4,
|
||||
borderTopColor: "#D8DDE7",
|
||||
borderBottomColor: "#D8DDE7",
|
||||
borderLeftColor: "#D8DDE7",
|
||||
borderRightColor: "#D8DDE7",
|
||||
height: 40,
|
||||
borderColor: "gray",
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 8,
|
||||
fontSize: 14,
|
||||
overflow: "hidden",
|
||||
fontFamily: "Inter",
|
||||
fontStyle: "normal",
|
||||
lineHeight: 20,
|
||||
fontWeight: "bold",
|
||||
backgroundColor: "#ffffff",
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Text, View, StyleSheet, TouchableOpacity } from "react-native";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { AppDispatch, RootState } from "../../store";
|
||||
import { clearVerifyOTPError, sendOTP, verifyOTP } from "../../store/authSlice";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { OtpInput } from "react-native-otp-entry";
|
||||
import { useRouter } from "expo-router";
|
||||
import { AUTH_STATUSES } from "@/constants/config";
|
||||
|
||||
export default function VerifyOTP() {
|
||||
const { phone, otpId, verifyOTPError, sendOTPError, status } = useSelector(
|
||||
(state: RootState) => state.auth
|
||||
);
|
||||
const router = useRouter();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
const [seconds, setSeconds] = useState(30);
|
||||
const [isTimerActive, setIsTimerActive] = useState(true);
|
||||
const [resendAttempts, setResendAttempts] = useState(0);
|
||||
const maxResendAttempts = 5;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(clearVerifyOTPError());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTimerActive && seconds > 0) {
|
||||
const timer = setInterval(() => {
|
||||
setSeconds((prev) => prev - 1);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
} else if (seconds === 0) {
|
||||
setIsTimerActive(false);
|
||||
}
|
||||
}, [seconds, isTimerActive]);
|
||||
|
||||
const resendOTP = async () => {
|
||||
if (resendAttempts < maxResendAttempts) {
|
||||
const newAttempts = resendAttempts + 1;
|
||||
setResendAttempts(newAttempts);
|
||||
setSeconds(30);
|
||||
setIsTimerActive(true);
|
||||
dispatch(sendOTP({ phone }));
|
||||
dispatch(clearVerifyOTPError());
|
||||
}
|
||||
};
|
||||
|
||||
const formattedSeconds = seconds.toString().padStart(2, "0");
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.heading}>Verify Code</Text>
|
||||
<View style={styles.body}>
|
||||
<Text>
|
||||
<Text style={styles.order}>
|
||||
Please enter verification code that we've sent to your number
|
||||
</Text>
|
||||
<Text style={styles.phone}> +91 ********{phone?.slice(-2)}.</Text>
|
||||
</Text>
|
||||
|
||||
<View style={styles.otpContainer}>
|
||||
<OtpInput
|
||||
numberOfDigits={4}
|
||||
focusColor="#006C4D"
|
||||
onTextChange={() => {
|
||||
dispatch(clearVerifyOTPError());
|
||||
}}
|
||||
onFilled={(code) => {
|
||||
dispatch(verifyOTP({ phone, otpId, otp: code }));
|
||||
}}
|
||||
autoFocus={true}
|
||||
type="numeric"
|
||||
focusStickBlinkingDuration={500}
|
||||
textInputProps={{
|
||||
accessibilityLabel: "One-Time Password",
|
||||
}}
|
||||
textProps={{
|
||||
accessibilityRole: "text",
|
||||
accessibilityLabel: "OTP digit",
|
||||
allowFontScaling: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
{(verifyOTPError || sendOTPError) && (
|
||||
<Text style={styles.error}>{verifyOTPError || sendOTPError}</Text>
|
||||
)}
|
||||
|
||||
<View style={styles.otpResend}>
|
||||
{isTimerActive ? (
|
||||
<Text>
|
||||
<Text style={styles.otpText}>Resend OTP in </Text>
|
||||
<Text style={styles.timer}>00:{formattedSeconds}</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={resendOTP}
|
||||
disabled={resendAttempts >= maxResendAttempts}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...styles.resendOTP,
|
||||
color:
|
||||
resendAttempts < maxResendAttempts
|
||||
? "#006C4D"
|
||||
: "#B0B7C5",
|
||||
}}
|
||||
>
|
||||
Resend OTP
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 16,
|
||||
paddingTop: 0,
|
||||
},
|
||||
heading: {
|
||||
fontSize: 28,
|
||||
fontStyle: "normal",
|
||||
fontWeight: "bold",
|
||||
lineHeight: 36.4,
|
||||
letterSpacing: -0.56,
|
||||
},
|
||||
body: {
|
||||
marginTop: 24,
|
||||
},
|
||||
phone: {
|
||||
color: "#252A34",
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
lineHeight: 20,
|
||||
},
|
||||
order: {
|
||||
color: "#949CAC",
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
lineHeight: 20,
|
||||
},
|
||||
otpContainer: {
|
||||
padding: 0,
|
||||
marginTop: 32,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
otpInput: {
|
||||
width: "80%",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
inputField: {
|
||||
borderColor: "#D8DDE7",
|
||||
borderRadius: 4,
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#252A34",
|
||||
textAlign: "center",
|
||||
fontFamily: "Inter",
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
height: 56,
|
||||
width: 56,
|
||||
},
|
||||
error: {
|
||||
color: "#D51D10",
|
||||
fontFamily: "Inter",
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
lineHeight: 20,
|
||||
marginTop: 8,
|
||||
},
|
||||
otpResend: {
|
||||
marginTop: 28,
|
||||
},
|
||||
otpText: {
|
||||
color: "#949CAC",
|
||||
fontFamily: "Inter",
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
timer: {
|
||||
color: "#252A34",
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
resendOTP: {
|
||||
fontFamily: "Inter",
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
// type PayloadAction<T> = {
|
||||
// type: string;
|
||||
// payload: T;
|
||||
// }
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import axios from "axios";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import urls, {
|
||||
STORAGE_KEYS,
|
||||
MESSAGES,
|
||||
StatusType,
|
||||
AUTH_STATUSES,
|
||||
} from "../constants/config";
|
||||
|
||||
interface AuthState {
|
||||
phone: string | null;
|
||||
otpId: number | null;
|
||||
isLoggedIn: boolean;
|
||||
status: StatusType;
|
||||
sendOTPError: string | null;
|
||||
verifyOTPError: string | null;
|
||||
generalError: string | null;
|
||||
}
|
||||
|
||||
interface SendOTPResponse {
|
||||
status: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
otpId: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface VerifyOTPResponse {
|
||||
status: boolean;
|
||||
message: string;
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
interface SendOTPParams {
|
||||
phone: string | null;
|
||||
}
|
||||
|
||||
interface VerifyOTPParams {
|
||||
phone: string | null;
|
||||
otp: string | null;
|
||||
otpId: number | null;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
phone: null,
|
||||
otpId: null,
|
||||
isLoggedIn: false,
|
||||
status: AUTH_STATUSES.IDLE,
|
||||
sendOTPError: null,
|
||||
verifyOTPError: null,
|
||||
generalError: null,
|
||||
};
|
||||
|
||||
//async thunk to send otp
|
||||
export const sendOTP = createAsyncThunk<SendOTPResponse, SendOTPParams>(
|
||||
"auth/sendOTP",
|
||||
async (params, { rejectWithValue }) => {
|
||||
try {
|
||||
console.log(urls.BASE_URL, "BASE_URL");
|
||||
// const response = await axios.post<SendOTPResponse>(
|
||||
// `${urls.BASE_URL}/send-otp`,
|
||||
// params
|
||||
// );
|
||||
// return response.data;
|
||||
return {
|
||||
status: true,
|
||||
message: "Done",
|
||||
data: {
|
||||
otpId: 22,
|
||||
},
|
||||
};
|
||||
// if (!response.data.status) throw new Error(response.data.message);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.response?.data?.message || error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const verifyOTP = createAsyncThunk<VerifyOTPResponse, VerifyOTPParams>(
|
||||
"auth/verifyOTP",
|
||||
async (params, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.post<VerifyOTPResponse>(
|
||||
`${urls.BASE_URL}/verify-otp`,
|
||||
params
|
||||
);
|
||||
if (!response.data.status) throw new Error(response.data.message);
|
||||
|
||||
// Store tokens
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, response.data.token);
|
||||
await SecureStore.setItemAsync(
|
||||
STORAGE_KEYS.REFRESH_TOKEN,
|
||||
response.data.refreshToken
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(MESSAGES.AUTHENTICATION.VERIFICATION_FAILED);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const logout = createAsyncThunk(
|
||||
"auth/logout",
|
||||
async (_, { dispatch }) => {
|
||||
await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
await SecureStore.deleteItemAsync(STORAGE_KEYS.REFRESH_TOKEN);
|
||||
dispatch(authSlice.actions.logout());
|
||||
}
|
||||
);
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
loginRequest: (state) => {
|
||||
state.status = AUTH_STATUSES.LOADING;
|
||||
},
|
||||
loginSuccess: (state, action) => {
|
||||
state.status = AUTH_STATUSES.SUCCESS;
|
||||
state.isLoggedIn = true; // Set logged-in state to true
|
||||
},
|
||||
setIsLoggedIn: (state, action) => {
|
||||
state.isLoggedIn = action.payload;
|
||||
},
|
||||
loginFailure: (state, action: PayloadAction<string>) => {
|
||||
state.status = AUTH_STATUSES.FAILED;
|
||||
state.generalError = action.payload;
|
||||
},
|
||||
logout: (state) => {
|
||||
state.isLoggedIn = false;
|
||||
},
|
||||
clearSendOTPError: (state) => {
|
||||
state.sendOTPError = null;
|
||||
},
|
||||
clearVerifyOTPError: (state) => {
|
||||
state.verifyOTPError = null;
|
||||
},
|
||||
clearAllErrors: (state) => {
|
||||
state.sendOTPError = null;
|
||||
state.verifyOTPError = null;
|
||||
state.generalError = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(sendOTP.pending, (state) => {
|
||||
state.status = AUTH_STATUSES.LOADING;
|
||||
state.sendOTPError = null;
|
||||
})
|
||||
.addCase(sendOTP.fulfilled, (state, action) => {
|
||||
state.status = AUTH_STATUSES.SUCCESS;
|
||||
state.otpId = action.payload.data.otpId;
|
||||
state.phone = action.meta.arg.phone;
|
||||
state.sendOTPError = null;
|
||||
})
|
||||
.addCase(sendOTP.rejected, (state, action) => {
|
||||
state.status = AUTH_STATUSES.FAILED;
|
||||
state.sendOTPError = action.error.message || "Failed to send OTP";
|
||||
})
|
||||
.addCase(verifyOTP.pending, (state) => {
|
||||
state.status = AUTH_STATUSES.LOADING;
|
||||
state.verifyOTPError = null;
|
||||
})
|
||||
.addCase(verifyOTP.fulfilled, (state, action) => {
|
||||
state.status = AUTH_STATUSES.SUCCESS;
|
||||
state.isLoggedIn = true;
|
||||
state.verifyOTPError = null;
|
||||
const token = action.payload.token;
|
||||
AsyncStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, token).catch((error) => {
|
||||
console.log("Error storing token", error);
|
||||
});
|
||||
|
||||
const refreshToken = action.payload.refreshToken;
|
||||
SecureStore.setItemAsync(
|
||||
STORAGE_KEYS.REFRESH_TOKEN,
|
||||
refreshToken
|
||||
).catch((error) => {
|
||||
console.log("Error storing refresh token", error);
|
||||
});
|
||||
})
|
||||
.addCase(verifyOTP.rejected, (state, action) => {
|
||||
state.status = AUTH_STATUSES.FAILED;
|
||||
state.verifyOTPError = action.error.message || "Failed to verify OTP";
|
||||
})
|
||||
.addCase(logout.pending, (state) => {
|
||||
state.status = AUTH_STATUSES.LOADING;
|
||||
})
|
||||
.addCase(logout.fulfilled, (state) => {
|
||||
state.otpId = null;
|
||||
state.isLoggedIn = false;
|
||||
state.status = AUTH_STATUSES.IDLE;
|
||||
state.verifyOTPError = null;
|
||||
state.generalError = null;
|
||||
state.sendOTPError = null;
|
||||
state.phone = null;
|
||||
})
|
||||
.addCase(logout.rejected, (state, action) => {
|
||||
state.status = AUTH_STATUSES.FAILED;
|
||||
state.generalError = action.error.message || "Failed to log out";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
logout: localLogout,
|
||||
clearSendOTPError,
|
||||
clearVerifyOTPError,
|
||||
clearAllErrors,
|
||||
} = authSlice.actions;
|
||||
|
||||
export default authSlice.reducer;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import rootReducer from "./rootReducer";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: rootReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: false,
|
||||
}),
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { combineReducers } from "@reduxjs/toolkit";
|
||||
import authreducer from "./authSlice";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
auth: authreducer,
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
Loading…
Reference in New Issue