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;