diff --git a/app/auth/_layout.tsx b/app/auth/_layout.tsx index 818715d..30eb0cc 100644 --- a/app/auth/_layout.tsx +++ b/app/auth/_layout.tsx @@ -7,8 +7,9 @@ import { useRouter } from "expo-router"; export default function AuthLayout() { const router = useRouter(); + return ( - + <> - + ); } // useEffect(() => { diff --git a/app/auth/verifyOtp.tsx b/app/auth/verifyOtp.tsx index f7195fe..fc3a7fc 100644 --- a/app/auth/verifyOtp.tsx +++ b/app/auth/verifyOtp.tsx @@ -63,22 +63,24 @@ export default function VerifyOTP() { {t("onboarding.enter-otp")} - +91 ********{phone?.slice(-2)}. + {phone}. { - dispatch(clearVerifyOTPError()); - }} - onFilled={(code) => { - dispatch(verifyOTP({ phone, otpId, otp: code })); - }} + focusColor="green" autoFocus={true} + hideStick={true} + blurOnFilled={true} + disabled={false} type="numeric" + secureTextEntry={false} focusStickBlinkingDuration={500} + onTextChange={(text) => console.log(text)} + onFilled={(text) => { + dispatch(verifyOTP({ otpId: `${otpId}`, otp: text, phone })); + }} textInputProps={{ accessibilityLabel: "One-Time Password", }} @@ -87,6 +89,14 @@ export default function VerifyOTP() { accessibilityLabel: "OTP digit", allowFontScaling: false, }} + theme={{ + containerStyle: styles.digitContainer, + pinCodeContainerStyle: styles.pinCodeContainer, + pinCodeTextStyle: styles.pinCodeText, + focusStickStyle: styles.focusStick, + placeholderTextStyle: styles.placeholderText, + disabledPinCodeContainerStyle: styles.disabledPinCodeContainer, + }} /> {(verifyOTPError || sendOTPError) && ( @@ -202,4 +212,48 @@ const styles = StyleSheet.create({ paddingTop: 8, paddingBottom: 8, }, + digitContainer: { + width: "100%", + flexDirection: "row", + justifyContent: "center", + gap: 16, + alignItems: "center", + }, + + pinCodeContainer: { + width: 56, + height: 56, + borderWidth: 1, + borderColor: "#D8DDE7", + borderRadius: 4, + backgroundColor: "#FFFFFF", + justifyContent: "center", + alignItems: "center", + }, + + disabledPinCodeContainer: { + backgroundColor: "#F2F4F7", + borderColor: "#E0E4EA", + }, + + pinCodeText: { + fontSize: 20, + fontWeight: "bold", + color: "#252A34", + textAlign: "center", + fontFamily: "Inter", + }, + + placeholderText: { + fontSize: 20, + color: "#B0B7C5", + textAlign: "center", + fontWeight: "bold", + }, + + focusStick: { + width: 2, + height: 24, + backgroundColor: "#252A34", + }, }); diff --git a/constants/config.ts b/constants/config.ts index dfc6626..6da754a 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -6,23 +6,19 @@ import ServiceIcon from "../assets/icons/service.svg"; import ServiceIconFilled from "../assets/icons/service-filled.svg"; import BatteryIcon from "../assets/icons/battery.svg"; import BatteryIconFilled from "../assets/icons/battery-filled.svg"; -import { BASE_URL, ENV } from "@env"; import InfoIcon from "../assets/icons/error.svg"; import WarningIcon from "../assets/icons/warning.svg"; import DangerIcon from "../assets/icons/danger.svg"; import type { BmsState } from "./types"; import { useTranslation } from "react-i18next"; -export default { - ENV, - BASE_URL, -}; +export const BASE_URL = + "https://eae2-2400-80c0-2001-9c6-50f0-eda-dcd7-4167.ngrok-free.app"; export const STORAGE_KEYS = { LANGUAGE: "userLanguage", AUTH_TOKEN: "authToken", THEME: "appTheme", - REFRESH_TOKEN: "refreshToken", }; export const APP_CONFIG = { diff --git a/services/axiosClient.ts b/services/axiosClient.ts new file mode 100644 index 0000000..dee8a33 --- /dev/null +++ b/services/axiosClient.ts @@ -0,0 +1,35 @@ +import axios from "axios"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { BASE_URL, STORAGE_KEYS } from "../constants/config"; +import { router } from "expo-router"; + +const api = axios.create({ + baseURL: BASE_URL, + timeout: 10000, +}); + +//Request interceptor to add auth token +api.interceptors.request.use(async (config) => { + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +//Response interceptor to handle errors +api.interceptors.response.use( + (response) => response, + async (error) => { + const status = error.response?.status; + //if token is expired or not present, clear it from storage + if (status === 401 || status === 403) { + await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); + router.replace("/auth/login"); + } + + return Promise.reject(error); + } +); + +export default api; diff --git a/services/i18n/index.ts b/services/i18n/index.ts index 1422ac2..005107e 100644 --- a/services/i18n/index.ts +++ b/services/i18n/index.ts @@ -30,9 +30,6 @@ export const setLanguage = async (language: string) => { export const getLanguage = async () => { const lang = await AsyncStorage.getItem(STORAGE_KEYS.LANGUAGE); - if (lang) { - i18next.changeLanguage(lang); - } return lang; }; diff --git a/services/socket.ts b/services/socket.ts index f7f12f3..516db8c 100644 --- a/services/socket.ts +++ b/services/socket.ts @@ -10,13 +10,13 @@ import { BmsState } from "@/constants/types"; const SERVER_URL = "http://dev.vec-tr.ai:8089/?dashboardId=deviceDashboardSocket&assetId=V16000868651064644504"; const TOKEN = - "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2NCIsImFjdGlvbiI6ImF1dGgiLCJ0b2tlbi12ZXJzaW9uIjowLCJpYXQiOjE3NTE1Mzg0MDAsImV4cCI6MTc1MTYyNDgwMH0.QIGyV9_jbtv0F8YzbzIgn_669HJz2ftI8KckpPGN0UU"; + "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2NCIsImFjdGlvbiI6ImF1dGgiLCJ0b2tlbi12ZXJzaW9uIjowLCJpYXQiOjE3NTE2MDgwNjIsImV4cCI6MTc1MTY5NDQ2Mn0.-46Br0jSPwOTvkcDBTI05GJ1GavaAOOli6LEgkvjj3c"; let socket: Socket | null = null; export const initSocket = () => { store.dispatch(setTelemetryLoading()); - + console.log("Initializing socket connection..."); socket = io(SERVER_URL, { transports: ["websocket"], extraHeaders: { @@ -54,13 +54,15 @@ const handleSocketData = (data: any) => { console.log("..."); try { const SoH = - data.dataSeries.assetData[0].bms[0].bmsSpecific.ivecSpecific.soh; - const SoC = data?.dataSeries?.assetData?.[0]?.bms?.[0]?.batterySoc; + data.dataSeries.assetData[0].bms[0].bmsSpecific.ivecSpecific.soh ?? null; + const SoC = data?.dataSeries?.assetData?.[0]?.bms?.[0]?.batterySoc ?? null; const currentMode = data?.dataSeries?.assetData?.[0]?.bms?.[0]?.bmsSpecific?.ivecSpecific - ?.ivecStatus?.currentMode; + ?.ivecStatus?.currentMode ?? null; - const gps = data?.dataSeries?.locationData?.gps; + const gps = data?.dataSeries?.locationData?.gps ?? null; + + const totalDistance = data?.dataSeries?.systemData?.odoMeter ?? null; const lat = gps?.[1]; const lon = gps?.[2]; @@ -84,6 +86,7 @@ const handleSocketData = (data: any) => { lon, loading: false, error: null, + totalDistance, }) ); } catch (err) { diff --git a/store/authSlice.ts b/store/authSlice.ts index fd27fec..9fa31fa 100644 --- a/store/authSlice.ts +++ b/store/authSlice.ts @@ -2,10 +2,10 @@ // type: string; // payload: T; // } +import axios from "axios"; import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; import AsyncStorage from "@react-native-async-storage/async-storage"; -import * as SecureStore from "expo-secure-store"; -import urls, { STORAGE_KEYS, MESSAGES } from "../constants/config"; +import { STORAGE_KEYS, MESSAGES, BASE_URL } from "../constants/config"; import { AUTH_STATUSES, StatusType } from "../constants/types"; interface AuthState { @@ -19,18 +19,23 @@ interface AuthState { } interface SendOTPResponse { - status: boolean; + success: boolean; message: string; data: { otpId: number; + phone: string; }; } interface VerifyOTPResponse { - status: boolean; + success: boolean; message: string; - token: string; - refreshToken: string; + data: { + success: boolean; + message: string; + token: string; + token_expires_in: string; + }; } interface SendOTPParams { @@ -40,7 +45,7 @@ interface SendOTPParams { interface VerifyOTPParams { phone: string | null; otp: string | null; - otpId: number | null; + otpId: string | null; } const initialState: AuthState = { @@ -58,22 +63,15 @@ export const sendOTP = createAsyncThunk( "auth/sendOTP", async (params, { rejectWithValue }) => { try { - console.log(urls.BASE_URL, "BASE_URL"); - // const response = await axios.post( - // `${urls.BASE_URL}/send-otp`, - // params - // ); - // return response.data; - return { - status: true, - message: "Done", - data: { - otpId: 22, - }, - }; + const response = await axios.post( + `${BASE_URL}/api/v1/send-otp`, + params + ); + return response.data; // if (!response.data.status) throw new Error(response.data.message); } catch (error: any) { - return rejectWithValue(error.response?.data?.message || error.message); + const serverMessage = error.response?.data?.message; + return rejectWithValue(serverMessage || "Something went wrong"); } } ); @@ -82,27 +80,22 @@ export const verifyOTP = createAsyncThunk( "auth/verifyOTP", async (params, { rejectWithValue }) => { try { - return { - status: true, - message: "Done", - token: "token", - refreshToken: "refreshToken", - }; - // const response = await axios.post( - // `${urls.BASE_URL}/verify-otp`, - // params - // ); - // if (!response.data.status) throw new Error(response.data.message); + console.log("params", params); + const response = await axios.post( + `${BASE_URL}/api/v1/verify-otp`, + params + ); + if (!response.data.success) 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 - // ); + // Store tokens + await AsyncStorage.setItem( + STORAGE_KEYS.AUTH_TOKEN, + response.data.data.token + ); - // return response.data; + return response.data; } catch (error: any) { + console.log(error.message); return rejectWithValue(MESSAGES.AUTHENTICATION.VERIFICATION_FAILED); } } @@ -112,7 +105,6 @@ 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()); } ); @@ -159,12 +151,12 @@ const authSlice = createSlice({ .addCase(sendOTP.fulfilled, (state, action) => { state.status = AUTH_STATUSES.SUCCESS; state.otpId = action.payload.data.otpId; - state.phone = action.meta.arg.phone; + state.phone = action.payload.data.phone; state.sendOTPError = null; }) .addCase(sendOTP.rejected, (state, action) => { state.status = AUTH_STATUSES.FAILED; - state.sendOTPError = action.error.message || "Failed to send OTP"; + state.sendOTPError = (action.payload as string) || "Failed to send OTP"; }) .addCase(verifyOTP.pending, (state) => { state.status = AUTH_STATUSES.LOADING; @@ -174,18 +166,10 @@ const authSlice = createSlice({ state.status = AUTH_STATUSES.SUCCESS; state.isLoggedIn = true; state.verifyOTPError = null; - const token = action.payload.token; + const token = action.payload.data.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; diff --git a/store/telemetrySlice.ts b/store/telemetrySlice.ts index a1e873c..c6115b2 100644 --- a/store/telemetrySlice.ts +++ b/store/telemetrySlice.ts @@ -9,6 +9,7 @@ interface TelemetryState { lon: number | null; loading: boolean; error: string | null; + totalDistance: number | null; } const initialState: TelemetryState = { @@ -19,6 +20,7 @@ const initialState: TelemetryState = { lon: null, loading: false, error: null, + totalDistance: null, }; export const telemetrySlice = createSlice({ diff --git a/store/userSlice.ts b/store/userSlice.ts new file mode 100644 index 0000000..da7c1bb --- /dev/null +++ b/store/userSlice.ts @@ -0,0 +1,86 @@ +// src/store/userSlice.ts + +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; +import api from "../services/axiosClient"; +import { BASE_URL } from "@/constants/config"; + +interface Vehicle { + vehicle_id: number; + model: string; + chasis_number: string; +} + +interface UserData { + user_id: number; + name: string; + mobile: string; + email: string | null; + profile_url: string | null; + vehicles: Vehicle[]; +} + +interface UserState { + data: UserData | null; + loading: boolean; + error: string | null; +} + +const initialState: UserState = { + data: null, + loading: false, + error: null, +}; + +// 🔁 Thunk to get user details +export const getUserDetails = createAsyncThunk( + "user/getUserDetails", + async (_, { rejectWithValue }) => { + try { + const response = await api.get(`${BASE_URL}/api/v1/get-user-details`); + if (response.data.success) { + return response.data.data; + } else { + return rejectWithValue("Failed to fetch user data"); + } + } catch (error: any) { + const message = + error.response?.data?.message || + error.message || + "Something went wrong"; + return rejectWithValue(message); + } + } +); + +const userSlice = createSlice({ + name: "user", + initialState, + reducers: { + clearUser: (state) => { + state.data = null; + state.error = null; + state.loading = false; + }, + }, + extraReducers: (builder) => { + builder + .addCase(getUserDetails.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase( + getUserDetails.fulfilled, + (state, action: PayloadAction) => { + state.loading = false; + state.data = action.payload; + } + ) + .addCase(getUserDetails.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string; + }); + }, +}); + +export const { clearUser } = userSlice.actions; +export default userSlice.reducer;