Fix env issue

feature/app-setup
vinay kumar 2025-07-05 22:59:29 +05:30
parent 4457adfbe5
commit eeb6683b59
16 changed files with 547 additions and 302 deletions

View File

@ -4,12 +4,30 @@ import { useTabConfig } from "@/constants/config";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "@/store"; import { RootState } from "@/store";
import { initSocket } from "@/services/socket"; import { initSocket } from "@/services/socket";
import { useSnackbar } from "@/contexts/Snackbar";
import NetInfo from "@react-native-community/netinfo";
export default function TabLayout() { export default function TabLayout() {
const { isLoggedIn } = useSelector((state: RootState) => state.auth); const { isLoggedIn } = useSelector((state: RootState) => state.auth);
if (!isLoggedIn) return null; if (!isLoggedIn) return null;
const TAB_CONFIG = useTabConfig(); const TAB_CONFIG = useTabConfig();
const { showSnackbar } = useSnackbar();
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
const isConnected = state.isConnected;
if (isConnected === false) {
console.log("No internet connection");
showSnackbar("No internet connection", "error");
}
});
return () => {
unsubscribe();
};
}, []);
useEffect(() => { useEffect(() => {
initSocket(); initSocket();
}, [isLoggedIn]); }, [isLoggedIn]);

View File

@ -29,9 +29,8 @@ export default function HomeScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigation = useNavigation(); const navigation = useNavigation();
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false); const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
const { SoC, SoH, chargingState, lat, lon, loading, error } = useSelector( const { SoC, SoH, chargingState, lat, lon, loading, error, totalDistance } =
(state: RootState) => state.telemetry useSelector((state: RootState) => state.telemetry);
);
const [prevPosition, setPrevPosition] = useState<{ const [prevPosition, setPrevPosition] = useState<{
lat: number; lat: number;
lon: number; lon: number;
@ -139,7 +138,7 @@ export default function HomeScreen() {
<MetricCard heading={t("home.battery-health")} value={SoH} unit="%" /> <MetricCard heading={t("home.battery-health")} value={SoH} unit="%" />
<MetricCard <MetricCard
heading={t("home.total-distance")} heading={t("home.total-distance")}
value={20009} value={totalDistance}
unit="km" unit="km"
/> />
</View> </View>
@ -156,7 +155,7 @@ export default function HomeScreen() {
<LocationOff /> <LocationOff />
<Text style={styles.errorText}>Fetching Location...</Text> <Text style={styles.errorText}>Fetching Location...</Text>
</View> </View>
) : lat && lon ? ( ) : lat != null && lon != null ? (
<> <>
<View style={styles.mapContainer}> <View style={styles.mapContainer}>
<MapView <MapView
@ -192,10 +191,12 @@ export default function HomeScreen() {
</TouchableOpacity> </TouchableOpacity>
</> </>
) : ( ) : (
<View style={styles.errorContainer}> error && (
<LocationOff /> <View style={styles.errorContainer}>
<Text style={styles.errorText}>Error fetching location</Text> <LocationOff />
</View> <Text style={styles.errorText}>Error fetching location</Text>
</View>
)
)} )}
</View> </View>

View File

@ -6,6 +6,8 @@ import "react-native-reanimated";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { store } from "@/store"; import { store } from "@/store";
import { PaperProvider } from "react-native-paper";
import SnackbarProvider from "@/contexts/Snackbar";
export { ErrorBoundary } from "expo-router"; export { ErrorBoundary } from "expo-router";
@ -55,17 +57,21 @@ export default function RootLayout() {
function RootLayoutNav() { function RootLayoutNav() {
return ( return (
<Provider store={store}> <PaperProvider>
<I18nextProvider i18n={i18next}> <SnackbarProvider>
<Stack <Provider store={store}>
screenOptions={{ <I18nextProvider i18n={i18next}>
headerShown: false, <Stack
animation: "fade", screenOptions={{
}} headerShown: false,
> animation: "fade",
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> }}
</Stack> >
</I18nextProvider> <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Provider> </Stack>
</I18nextProvider>
</Provider>
</SnackbarProvider>
</PaperProvider>
); );
} }

View File

@ -8,8 +8,8 @@ import {
TouchableWithoutFeedback, TouchableWithoutFeedback,
Keyboard, Keyboard,
KeyboardAvoidingView, KeyboardAvoidingView,
StatusBar,
Linking, Linking,
Platform,
} from "react-native"; } from "react-native";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { Formik } from "formik"; import { Formik } from "formik";
@ -22,16 +22,8 @@ import { RootState } from "../../store";
import { AUTH_STATUSES } from "@/constants/types"; import { AUTH_STATUSES } from "@/constants/types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SUPPORT } from "@/constants/config"; import { SUPPORT } from "@/constants/config";
// import { useNavigation } from "expo-router"; import { useSafeAreaInsets } from "react-native-safe-area-context";
// 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() { export default function WelcomeScreen() {
const { const {
status, status,
@ -42,6 +34,8 @@ export default function WelcomeScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets();
// const navigation = useNavigation(); // const navigation = useNavigation();
const phoneValidationSchema = Yup.object().shape({ const phoneValidationSchema = Yup.object().shape({
@ -63,10 +57,13 @@ export default function WelcomeScreen() {
}; };
return ( return (
<KeyboardAvoidingView style={styles.container} behavior="padding"> <KeyboardAvoidingView
<StatusBar barStyle="dark-content" backgroundColor="#F3F5F8" /> style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 0}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}> <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.inner}> <View style={[styles.inner, { paddingBottom: insets.bottom }]}>
<Text style={styles.title}>{t("onboarding.welcome")}</Text> <Text style={styles.title}>{t("onboarding.welcome")}</Text>
<Formik <Formik
@ -82,64 +79,70 @@ export default function WelcomeScreen() {
errors, errors,
touched, touched,
}) => ( }) => (
<View style={styles.form}> <View style={styles.formContainer}>
<Text style={styles.label}> <View style={styles.inputContainer}>
{t("onboarding.enter-mobile-number")} <Text style={styles.label}>
</Text> {t("onboarding.enter-mobile-number")}
<TextInput </Text>
style={{ <TextInput
...styles.input, style={{
color: values.phone ? "black" : "#949CAC", ...styles.input,
}} color: values.phone ? "black" : "#949CAC",
onChangeText={(text) => { }}
handleChange("phone")(text); onChangeText={(text) => {
if (sendOTPError) { handleChange("phone")(text);
dispatch(clearSendOTPError()); if (sendOTPError) {
} dispatch(clearSendOTPError());
}} }
onBlur={handleBlur("phone")} }}
value={values.phone} onBlur={handleBlur("phone")}
keyboardType="numeric" value={values.phone}
placeholder={t("onboarding.enter-registered-mobile-number")} keyboardType="numeric"
placeholderTextColor={"#949CAC"} placeholder={t("onboarding.enter-registered-mobile-number")}
/> placeholderTextColor={"#949CAC"}
<View style={styles.errorContainer}> />
{touched.phone && errors.phone && ( <View style={styles.errorContainer}>
<Text style={styles.error}>{errors.phone}</Text> {touched.phone && errors.phone && (
)} <Text style={styles.error}>{errors.phone}</Text>
{sendOTPError && ( )}
<Text style={styles.error}>{sendOTPError}</Text> {sendOTPError && (
)} <Text style={styles.error}>{sendOTPError}</Text>
</View> )}
<View style={styles.contactContainer}>
<View style={{ flexDirection: "row" }}>
<Text>For any queries, </Text>
<TouchableOpacity onPress={makePhoneCall}>
<Text style={styles.link}>contact us.</Text>
</TouchableOpacity>
</View> </View>
</View> </View>
<TouchableOpacity
onPress={handleSubmit as unknown as () => void} <View style={styles.bottomSection}>
style={{ <View style={styles.contactContainer}>
...styles.button, <View style={{ flexDirection: "row" }}>
backgroundColor: <Text>For any queries, </Text>
values.phone.length === 10 && <TouchableOpacity onPress={makePhoneCall}>
!errors.phone && <Text style={styles.link}>contact us.</Text>
status !== AUTH_STATUSES.LOADING </TouchableOpacity>
? "#008761" </View>
: "#B0B7C5", </View>
}}
disabled={ <TouchableOpacity
values.phone.length !== 10 || onPress={handleSubmit as unknown as () => void}
!!errors.phone || style={{
status === AUTH_STATUSES.LOADING ...styles.button,
} backgroundColor:
> values.phone.length === 10 &&
<Text style={styles.buttonText}> !errors.phone &&
{t("onboarding.send-otp")} status !== AUTH_STATUSES.LOADING
</Text> ? "#008761"
</TouchableOpacity> : "#B0B7C5",
}}
disabled={
values.phone.length !== 10 ||
!!errors.phone ||
status === AUTH_STATUSES.LOADING
}
>
<Text style={styles.buttonText}>
{t("onboarding.send-otp")}
</Text>
</TouchableOpacity>
</View>
</View> </View>
)} )}
</Formik> </Formik>
@ -150,12 +153,29 @@ export default function WelcomeScreen() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: "#F3F5F8",
},
inner: {
flex: 1,
},
formContainer: {
flex: 1,
justifyContent: "space-between",
},
inputContainer: {
flex: 1,
},
bottomSection: {
width: "100%",
},
contactContainer: { contactContainer: {
width: "100%", width: "100%",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
position: "absolute", marginBottom: 16,
bottom: 110, // slightly above the "Send OTP" button
}, },
link: { link: {
fontSize: 14, fontSize: 14,
@ -175,24 +195,12 @@ const styles = StyleSheet.create({
fontWeight: "bold", fontWeight: "bold",
lineHeight: 20, lineHeight: 20,
}, },
inner: {
flex: 1,
justifyContent: "flex-start",
position: "relative",
},
form: {
height: "90%",
position: "relative",
},
button: { button: {
height: 48, height: 48,
borderRadius: 4, borderRadius: 4,
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
marginTop: 20,
width: "100%", width: "100%",
position: "absolute",
bottom: 50,
}, },
buttonText: { buttonText: {
color: "#FCFCFC", color: "#FCFCFC",
@ -201,12 +209,6 @@ const styles = StyleSheet.create({
fontWeight: "bold", fontWeight: "bold",
lineHeight: 20, lineHeight: 20,
}, },
container: {
flex: 1,
padding: 16,
backgroundColor: "#F3F5F8",
paddingTop: 0,
},
title: { title: {
fontSize: 28, fontSize: 28,
fontWeight: "bold", fontWeight: "bold",

View File

@ -2,11 +2,14 @@ import React, { useEffect, useState } from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native"; import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { setLanguage } from "../../services/i18n/index"; import { setLanguage } from "../../services/i18n/index";
import { useRouter, useNavigation } from "expo-router"; import { useRouter, useNavigation } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const LanguageScreen = () => { const LanguageScreen = () => {
const router = useRouter(); const router = useRouter();
const navigation = useNavigation(); const navigation = useNavigation();
const [selectedLang, setSelectedLang] = useState<string | null>(null); const [selectedLang, setSelectedLang] = useState<string | null>(null);
const insets = useSafeAreaInsets();
useEffect(() => { useEffect(() => {
navigation.setOptions({ headerShown: false }); navigation.setOptions({ headerShown: false });
@ -24,41 +27,47 @@ const LanguageScreen = () => {
}; };
return ( return (
<View style={styles.container}> <>
<Text style={styles.title}>Select Language</Text> <StatusBar style="dark" />
<Text style={styles.subtitle}>Please select your preferred language</Text> <View style={styles.container}>
<Text style={styles.title}>Select Language</Text>
<Text style={styles.subtitle}>
Please select your preferred language
</Text>
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.languageCard, styles.languageCard,
selectedLang === "en" && styles.selectedCard, selectedLang === "en" && styles.selectedCard,
]} ]}
onPress={() => handleLanguagePress("en")} onPress={() => handleLanguagePress("en")}
> >
<Text style={styles.languageText}>English</Text> <Text style={styles.languageText}>English</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.languageCard, styles.languageCard,
selectedLang === "hi" && styles.selectedCard, selectedLang === "hi" && styles.selectedCard,
]} ]}
onPress={() => handleLanguagePress("hi")} onPress={() => handleLanguagePress("hi")}
> >
<Text style={styles.languageText}>ि</Text> <Text style={styles.languageText}>ि</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.selectButton, styles.selectButton,
selectedLang ? styles.selectEnabled : styles.selectDisabled, selectedLang ? styles.selectEnabled : styles.selectDisabled,
]} { marginBottom: insets.bottom + 16 },
onPress={handleSelect} ]}
disabled={!selectedLang} onPress={handleSelect}
> disabled={!selectedLang}
<Text style={styles.selectButtonText}>Select</Text> >
</TouchableOpacity> <Text style={styles.selectButtonText}>Select</Text>
</View> </TouchableOpacity>
</View>
</>
); );
}; };
@ -68,7 +77,6 @@ const styles = StyleSheet.create({
backgroundColor: "#f3f5f8", backgroundColor: "#f3f5f8",
paddingTop: 108, paddingTop: 108,
paddingHorizontal: 16, paddingHorizontal: 16,
paddingBottom: 80,
}, },
title: { title: {
fontSize: 28, fontSize: 28,
@ -91,7 +99,7 @@ const styles = StyleSheet.create({
justifyContent: "center", justifyContent: "center",
}, },
selectedCard: { selectedCard: {
borderColor: "#008000", borderColor: "#009E71",
}, },
languageText: { languageText: {
fontSize: 16, fontSize: 16,
@ -99,6 +107,7 @@ const styles = StyleSheet.create({
}, },
selectButton: { selectButton: {
marginTop: "auto", marginTop: "auto",
marginBottom: 16,
padding: 16, padding: 16,
borderRadius: 4, borderRadius: 4,
alignItems: "center", alignItems: "center",

View File

@ -8,56 +8,68 @@ import {
ScrollView, ScrollView,
} from "react-native"; } from "react-native";
import { MaterialIcons } from "@expo/vector-icons"; import { MaterialIcons } from "@expo/vector-icons";
import LanguageModal from "@/components/Profile/LangaugeModal";
export default function ProfileScreen() { export default function ProfileScreen() {
const [isLangaugeModalVisible, setLanguageModalVisible] =
React.useState(false);
const toggleLanguageModal = () => {
setLanguageModalVisible(!isLangaugeModalVisible);
};
return ( return (
<ScrollView contentContainerStyle={styles.scrollContent}> <>
<View style={styles.avatarContainer}> <ScrollView contentContainerStyle={styles.scrollContent}>
<Image <View style={styles.avatarContainer}>
source={require("../../assets/images/user_image.jpg")} <Image
style={styles.avatar} source={require("../../assets/images/user_image.jpg")}
/> style={styles.avatar}
<TouchableOpacity style={styles.editAvatar}> />
<MaterialIcons name="edit" size={20} color="#FDFDFD" /> <TouchableOpacity style={styles.editAvatar}>
</TouchableOpacity> <MaterialIcons name="edit" size={20} color="#FDFDFD" />
</View>
<View style={styles.card}>
<View style={styles.row}>
<View style={styles.textGroup}>
<Text style={styles.label}>Name</Text>
<Text style={styles.value}>Amar Kesari</Text>
</View>
<TouchableOpacity>
<MaterialIcons name="edit" size={20} color="#555C70" />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={styles.divider} /> <View style={styles.card}>
<View style={styles.row}> <View style={styles.row}>
<View style={styles.textGroup}> <View style={styles.textGroup}>
<Text style={styles.label}>Mobile Number</Text> <Text style={styles.label}>Name</Text>
<Text style={styles.value}>9876543210</Text> <Text style={styles.value}>Amar Kesari</Text>
</View>
<TouchableOpacity>
<MaterialIcons name="edit" size={20} color="#555C70" />
</TouchableOpacity>
</View>
<View style={styles.divider} />
<View style={styles.row}>
<View style={styles.textGroup}>
<Text style={styles.label}>Mobile Number</Text>
<Text style={styles.value}>9876543210</Text>
</View>
</View> </View>
</View> </View>
</View>
{/* Other Menu Items */} <View style={styles.card}>
<View style={styles.card}> {menuItem("My Vehicle")}
{menuItem("My Vehicle")} <View style={styles.divider} />
<View style={styles.divider} /> {menuItem("Language", () => toggleLanguageModal())}
{menuItem("Language")} </View>
</View>
<View style={styles.card}> <View style={styles.card}>
{menuItem("About App")} {menuItem("About App")}
<View style={styles.divider} /> <View style={styles.divider} />
{menuItem("Logout")} {menuItem("Logout")}
</View> </View>
</ScrollView> </ScrollView>
<LanguageModal
onClose={() => setLanguageModalVisible(false)}
visible={isLangaugeModalVisible}
/>
</>
); );
} }
const menuItem = (title: string) => ( const menuItem = (title: string, onPress?: () => void) => (
<TouchableOpacity style={styles.menuRow}> <TouchableOpacity style={styles.menuRow} onPress={onPress}>
<Text style={styles.menuText}>{title}</Text> <Text style={styles.menuText}>{title}</Text>
<MaterialIcons name="chevron-right" size={20} color="#555C70" /> <MaterialIcons name="chevron-right" size={20} color="#555C70" />
</TouchableOpacity> </TouchableOpacity>

6
babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
};
};

View File

@ -0,0 +1,103 @@
import BottomSheetModal from "@/components/common/BottomSheetModal"; // adjust path
import { StyleSheet, Text, TouchableOpacity } from "react-native";
import { useEffect, useState } from "react";
import { getLanguage, setLanguage } from "@/services/i18n";
interface CustomerSupportProps {
visible: boolean;
onClose: () => void;
}
export default function LanguageModal({
visible,
onClose,
}: CustomerSupportProps) {
const [selectedLang, setSelectedLang] = useState<"en" | "hi">("en");
useEffect(() => {
(async () => {
const lang = await getLanguage();
setSelectedLang(lang === "hi" ? "hi" : "en");
})();
}, []);
const handleLanguagePress = (lang: "en" | "hi") => {
setSelectedLang(lang);
setLanguage(lang);
onClose();
};
return (
<BottomSheetModal
visible={visible}
onClose={onClose}
heading="Select Language"
>
<TouchableOpacity
style={[
styles.languageCard,
selectedLang === "en" && styles.selectedCard,
]}
onPress={() => handleLanguagePress("en")}
>
<Text style={styles.languageText}>English</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.languageCard,
selectedLang === "hi" && styles.selectedCard,
]}
onPress={() => handleLanguagePress("hi")}
>
<Text style={styles.languageText}>ि</Text>
</TouchableOpacity>
</BottomSheetModal>
);
}
const styles = StyleSheet.create({
languageText: {
fontSize: 16,
color: "#2f465e",
},
selectedCard: {
borderColor: "#009E71",
},
languageCard: {
width: "100%",
borderWidth: 1,
borderColor: "#d8dde7",
borderRadius: 4,
padding: 16,
marginBottom: 16,
justifyContent: "center",
},
row: {
flexDirection: "row",
gap: 16,
justifyContent: "space-between",
},
secondaryButton: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingVertical: 8,
paddingHorizontal: 16,
height: 40,
borderRadius: 4,
backgroundColor: "#F3F5F8",
borderWidth: 1,
borderColor: "#D8DDE7",
width: 156,
gap: 8,
},
fullButton: {
width: "100%",
},
buttonText: {
fontSize: 14,
fontWeight: "500",
color: "#252A34",
},
});

View File

@ -0,0 +1,87 @@
import React from "react";
import {
View,
Text,
StyleSheet,
Modal,
Pressable,
StyleProp,
ViewStyle,
} from "react-native";
import { StatusBar } from "expo-status-bar";
import CloseIcon from "../../assets/icons/close.svg";
type Props = {
visible: boolean;
onClose: () => void;
heading: string;
children: React.ReactNode;
containerStyle?: StyleProp<ViewStyle>;
};
const BottomSheetModal: React.FC<Props> = ({
visible,
onClose,
heading,
children,
containerStyle,
}) => {
return (
<Modal visible={visible} animationType="fade" transparent>
<StatusBar style="dark" />
<View style={styles.overlay}>
<View style={[styles.modalContainer, containerStyle]}>
<View style={styles.header}>
<Text style={styles.headerText}>{heading}</Text>
<Pressable onPress={onClose} style={styles.iconButton}>
<CloseIcon />
</Pressable>
</View>
<View style={styles.content}>{children}</View>
</View>
</View>
</Modal>
);
};
export default BottomSheetModal;
const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: "flex-end",
backgroundColor: "rgba(0,0,0,0.5)",
},
modalContainer: {
backgroundColor: "#fff",
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
paddingHorizontal: 16,
paddingVertical: 16,
},
header: {
borderBottomWidth: 1,
borderBottomColor: "#E5E9F0",
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
headerText: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
},
iconButton: {
width: 40,
height: 40,
padding: 10,
justifyContent: "center",
alignItems: "center",
},
content: {
paddingVertical: 16,
gap: 16,
},
});

View File

@ -31,7 +31,7 @@ const CustomSnackbar: React.FC<CustomSnackbarProps> = ({
return ( return (
<Snackbar <Snackbar
visible={true} visible={visible}
onDismiss={onDismiss} onDismiss={onDismiss}
style={[ style={[
styles.snackbar, styles.snackbar,

View File

@ -16,7 +16,7 @@ const BatteryStatus: React.FC<BatteryStatusProps> = ({ status, style }) => {
case 1: case 1:
return { return {
text: "Charging", text: "Charging",
backgroundColor: "#006C4D", backgroundColor: "#DAF5ED",
textColor: "#006C4D", textColor: "#006C4D",
}; };
case -1: case -1:
@ -28,20 +28,14 @@ const BatteryStatus: React.FC<BatteryStatusProps> = ({ status, style }) => {
case 0: case 0:
return { return {
text: "Idle", text: "Idle",
backgroundColor: "#565F70", backgroundColor: "#D8DDE7",
textColor: "#565F70", textColor: "#565F70",
}; };
case null:
return {
text: "---",
backgroundColor: "#e2e3e5",
textColor: "#6c757d",
};
default: default:
return { return {
text: "Unknown", text: "---",
backgroundColor: "#f8f9fa", backgroundColor: "#D8DDE7",
textColor: "#6c757d", textColor: "#565F70",
}; };
} }
}; };

View File

@ -1,31 +1,23 @@
import React from "react"; import BottomSheetModal from "@/components/common/BottomSheetModal"; // adjust path
import {
View,
Text,
StyleSheet,
Pressable,
Linking,
Modal,
} from "react-native";
import WhatsappIcon from "../../assets/icons/whatsapp.svg"; import WhatsappIcon from "../../assets/icons/whatsapp.svg";
import CallIcon from "../../assets/icons/call.svg"; import CallIcon from "../../assets/icons/call.svg";
import EmailIcon from "../../assets/icons/mail.svg"; import EmailIcon from "../../assets/icons/mail.svg";
import CloseIcon from "../../assets/icons/close.svg"; import { Linking, Pressable, StyleSheet, Text, View } from "react-native";
import { SUPPORT } from "@/constants/config"; import { SUPPORT } from "@/constants/config";
import { StatusBar } from "expo-status-bar";
type Props = { interface CustomerSupportProps {
visible: boolean; visible: boolean;
onClose: () => void; onClose: () => void;
}; }
const CustomerSupportModal: React.FC<Props> = ({ visible, onClose }) => { export default function CustomerSupport({
const openWhatsApp = async () => { visible,
onClose,
}: CustomerSupportProps) {
const openWhatsApp = () => {
const url = `https://wa.me/${ const url = `https://wa.me/${
SUPPORT.WHATSAPP_NUMBER SUPPORT.WHATSAPP_NUMBER
}?text=${encodeURIComponent(SUPPORT.WHATSAPP_PLACEHOLDER)}`; }?text=${encodeURIComponent(SUPPORT.WHATSAPP_PLACEHOLDER)}`;
Linking.openURL(url).catch((err) => Linking.openURL(url).catch((err) =>
console.log("Failed to open WhatsApp:", err) console.log("Failed to open WhatsApp:", err)
); );
@ -43,84 +35,33 @@ const CustomerSupportModal: React.FC<Props> = ({ visible, onClose }) => {
}; };
return ( return (
<Modal visible={visible} animationType="fade" transparent> <BottomSheetModal
<StatusBar style="dark" /> visible={visible}
<View style={styles.overlay}> onClose={onClose}
<View style={styles.modalContainer}> heading="Customer Support"
{/* Header */} >
<View style={styles.header}> <View style={styles.row}>
<Text style={styles.headerText}>Customer Support</Text> <Pressable style={styles.secondaryButton} onPress={openWhatsApp}>
<Pressable onPress={onClose} style={styles.iconButton}> <WhatsappIcon />
<CloseIcon /> <Text style={styles.buttonText}>Whatsapp</Text>
</Pressable> </Pressable>
</View> <Pressable style={styles.secondaryButton} onPress={makePhoneCall}>
<CallIcon />
{/* Buttons */} <Text style={styles.buttonText}>Call Us</Text>
<View style={styles.content}> </Pressable>
<View style={styles.row}>
<Pressable style={styles.secondaryButton} onPress={openWhatsApp}>
<WhatsappIcon />
<Text style={styles.buttonText}>Whatsapp</Text>
</Pressable>
<Pressable style={styles.secondaryButton} onPress={makePhoneCall}>
<CallIcon />
<Text style={styles.buttonText}>Call Us</Text>
</Pressable>
</View>
<Pressable
style={[styles.secondaryButton, styles.fullButton]}
onPress={sendEmail}
>
<EmailIcon />
<Text style={styles.buttonText}>Email</Text>
</Pressable>
</View>
</View>
</View> </View>
</Modal> <Pressable
style={[styles.secondaryButton, styles.fullButton]}
onPress={sendEmail}
>
<EmailIcon />
<Text style={styles.buttonText}>Email</Text>
</Pressable>
</BottomSheetModal>
); );
}; }
export default CustomerSupportModal;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: "flex-end",
backgroundColor: "rgba(0,0,0,0.5)",
},
modalContainer: {
backgroundColor: "#fff",
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
paddingHorizontal: 16,
paddingVertical: 16,
},
header: {
borderBottomWidth: 1,
borderBottomColor: "#E5E9F0",
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
headerText: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
},
iconButton: {
width: 40,
height: 40,
padding: 10,
justifyContent: "center",
alignItems: "center",
},
content: {
paddingVertical: 16,
gap: 16,
},
row: { row: {
flexDirection: "row", flexDirection: "row",
gap: 16, gap: 16,

View File

@ -13,7 +13,7 @@ const MetricCard: React.FC<MetricCardProps> = ({ heading, value, unit }) => {
<View style={styles.textContainer}> <View style={styles.textContainer}>
<Text style={styles.heading}>{heading}</Text> <Text style={styles.heading}>{heading}</Text>
<Text style={styles.value}> <Text style={styles.value}>
{value ?? "---"} {unit} {value?.toFixed(2) ?? "---"} {unit}
</Text> </Text>
</View> </View>
</View> </View>

View File

@ -3,13 +3,13 @@
"development": { "development": {
"env": { "env": {
"ENV": "development", "ENV": "development",
"BASE_URL": "https://dev-api-service.vecmocon.com/service-buddy" "BASE_URL": "https://dev-driver-saathi-api.vecmocon.com"
} }
}, },
"production": { "production": {
"env": { "env": {
"ENV": "production", "ENV": "production",
"BASE_URL": "https://dev-api-service.vecmocon.com/service-buddy" "BASE_URL": "https://dev-driver-saathi-api.vecmocon.com"
} }
} }
} }

91
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@expo/vector-icons": "^14.1.0", "@expo/vector-icons": "^14.1.0",
"@react-native-async-storage/async-storage": "^2.2.0", "@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"@react-navigation/stack": "^7.4.2", "@react-navigation/stack": "^7.4.2",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
@ -30,9 +31,9 @@
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-i18next": "^15.5.3", "react-i18next": "^15.5.3",
"react-native": "0.79.4", "react-native": "0.79.4",
"react-native-dotenv": "^3.4.11",
"react-native-maps": "^1.24.3", "react-native-maps": "^1.24.3",
"react-native-otp-entry": "^1.8.5", "react-native-otp-entry": "^1.8.5",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "~3.17.4", "react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "^5.4.0", "react-native-safe-area-context": "^5.4.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.11.1",
@ -1537,6 +1538,28 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@callstack/react-theme-provider": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz",
"integrity": "sha512-tTQ0uDSCL0ypeMa8T/E9wAZRGKWj8kXP7+6RYgPTfOPs9N07C9xM8P02GJ3feETap4Ux5S69D9nteq9mEj86NA==",
"license": "MIT",
"dependencies": {
"deepmerge": "^3.2.0",
"hoist-non-react-statics": "^3.3.0"
},
"peerDependencies": {
"react": ">=16.3.0"
}
},
"node_modules/@callstack/react-theme-provider/node_modules/deepmerge": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz",
"integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@egjs/hammerjs": { "node_modules/@egjs/hammerjs": {
"version": "2.0.17", "version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
@ -3324,6 +3347,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@react-native-community/netinfo": {
"version": "11.4.1",
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz",
"integrity": "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.59"
}
},
"node_modules/@react-native/assets-registry": { "node_modules/@react-native/assets-registry": {
"version": "0.79.4", "version": "0.79.4",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.4.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.4.tgz",
@ -11816,18 +11848,6 @@
} }
} }
}, },
"node_modules/react-native-dotenv": {
"version": "3.4.11",
"resolved": "https://registry.npmjs.org/react-native-dotenv/-/react-native-dotenv-3.4.11.tgz",
"integrity": "sha512-6vnIE+WHABSeHCaYP6l3O1BOEhWxKH6nHAdV7n/wKn/sciZ64zPPp2NUdEUf1m7g4uuzlLbjgr+6uDt89q2DOg==",
"license": "MIT",
"dependencies": {
"dotenv": "^16.4.5"
},
"peerDependencies": {
"@babel/runtime": "^7.20.6"
}
},
"node_modules/react-native-edge-to-edge": { "node_modules/react-native-edge-to-edge": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz", "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz",
@ -11896,6 +11916,51 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-paper": {
"version": "5.14.5",
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.14.5.tgz",
"integrity": "sha512-eaIH5bUQjJ/mYm4AkI6caaiyc7BcHDwX6CqNDi6RIxfxfWxROsHpll1oBuwn/cFvknvA8uEAkqLk/vzVihI3AQ==",
"license": "MIT",
"workspaces": [
"example",
"docs"
],
"dependencies": {
"@callstack/react-theme-provider": "^3.0.9",
"color": "^3.1.2",
"use-latest-callback": "^0.2.3"
},
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-safe-area-context": "*"
}
},
"node_modules/react-native-paper/node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"license": "MIT",
"dependencies": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"node_modules/react-native-paper/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"license": "MIT",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/react-native-paper/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT"
},
"node_modules/react-native-reanimated": { "node_modules/react-native-reanimated": {
"version": "3.17.5", "version": "3.17.5",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz",

View File

@ -15,6 +15,7 @@
"dependencies": { "dependencies": {
"@expo/vector-icons": "^14.1.0", "@expo/vector-icons": "^14.1.0",
"@react-native-async-storage/async-storage": "^2.2.0", "@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"@react-navigation/stack": "^7.4.2", "@react-navigation/stack": "^7.4.2",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
@ -35,9 +36,9 @@
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-i18next": "^15.5.3", "react-i18next": "^15.5.3",
"react-native": "0.79.4", "react-native": "0.79.4",
"react-native-dotenv": "^3.4.11",
"react-native-maps": "^1.24.3", "react-native-maps": "^1.24.3",
"react-native-otp-entry": "^1.8.5", "react-native-otp-entry": "^1.8.5",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "~3.17.4", "react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "^5.4.0", "react-native-safe-area-context": "^5.4.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.11.1",