Add login functionality
parent
4168f1a4b7
commit
189f8bd673
|
|
@ -1,20 +1,20 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
import { Tabs } from "expo-router";
|
||||||
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 { TAB_CONFIG } from "@/constants/config";
|
import { TAB_CONFIG } from "@/constants/config";
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
|
tabBarStyle: {
|
||||||
headerShown: useClientOnlyValue(false, true),
|
backgroundColor: "#252A34",
|
||||||
|
borderTopLeftRadius: 12,
|
||||||
|
borderTopRightRadius: 12,
|
||||||
|
position: "absolute",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
tabBarActiveTintColor: "#fff",
|
||||||
|
tabBarInactiveTintColor: "#aaa",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{TAB_CONFIG.map(({ name, title, Icon, IconFilled }) => (
|
{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 { useTranslation } from "react-i18next";
|
||||||
import EditScreenInfo from "@/components/EditScreenInfo";
|
import SemiCircleProgress from "../../components/home/SemiCircleProgress";
|
||||||
import { Text, View } from "@/components/Themed";
|
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 { t } = useTranslation();
|
||||||
return (
|
const navigation = useNavigation();
|
||||||
<View style={styles.container}>
|
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
|
||||||
<Text style={styles.title}>{t("welcome")}</Text>
|
|
||||||
<Text style={styles.title}>{t("change-language")}</Text>
|
useLayoutEffect(() => {
|
||||||
<View
|
navigation.setOptions({
|
||||||
style={styles.separator}
|
headerTitle: () => (
|
||||||
lightColor="#eee"
|
<View style={styles.headerTitleContainer}>
|
||||||
darkColor="rgba(255,255,255,0.1)"
|
<Text style={styles.title}>Yatri - NBX 600</Text>
|
||||||
/>
|
<Text style={styles.subtitle}>DL253C3602</Text>
|
||||||
<EditScreenInfo path="app/(tabs)/index.tsx" />
|
|
||||||
</View>
|
</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 (
|
||||||
|
<>
|
||||||
|
<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)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
metrics: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
backgroundColor: "#F3F5F8",
|
||||||
|
},
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: "center",
|
backgroundColor: "#F3F5F8",
|
||||||
justifyContent: "center",
|
padding: 16,
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
headerTitleContainer: {
|
||||||
|
flexDirection: "column",
|
||||||
|
backgroundColor: "#fff",
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 20,
|
fontSize: 14,
|
||||||
fontWeight: "bold",
|
color: "#6B7280",
|
||||||
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
separator: {
|
subtitle: {
|
||||||
marginVertical: 30,
|
fontSize: 18,
|
||||||
height: 1,
|
color: "#111827",
|
||||||
width: "80%",
|
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 { useEffect, useState } from "react";
|
||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
import { useColorScheme } from "@/components/useColorScheme";
|
import { store } from "@/store";
|
||||||
|
|
||||||
export { ErrorBoundary } from "expo-router";
|
export { ErrorBoundary } from "expo-router";
|
||||||
|
|
||||||
|
|
@ -48,6 +48,7 @@ export default function RootLayout() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loaded && appIsReady) {
|
if (loaded && appIsReady) {
|
||||||
SplashScreen.hideAsync();
|
SplashScreen.hideAsync();
|
||||||
|
router.replace("/auth/login");
|
||||||
}
|
}
|
||||||
}, [loaded, appIsReady]);
|
}, [loaded, appIsReady]);
|
||||||
|
|
||||||
|
|
@ -68,11 +69,13 @@ export default function RootLayout() {
|
||||||
|
|
||||||
function RootLayoutNav() {
|
function RootLayoutNav() {
|
||||||
return (
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
<I18nextProvider i18n={i18next}>
|
<I18nextProvider i18n={i18next}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
|
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</I18nextProvider>
|
</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