Add Edit Name

feature/app-setup
vinay kumar 2025-07-07 12:16:01 +05:30
parent ad7d59b7a5
commit b7ce82b86e
7 changed files with 246 additions and 10 deletions

View File

@ -11,6 +11,7 @@ import * as SplashScreen from "expo-splash-screen";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { STORAGE_KEYS } from "@/constants/config";
import { setIsLoggedIn } from "@/store/authSlice";
import { getUserDetails } from "@/store/userSlice";
SplashScreen.preventAutoHideAsync();
@ -35,11 +36,29 @@ function SplashAndAuthRouter() {
useEffect(() => {
const loadAuth = async () => {
const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
dispatch(setIsLoggedIn(!!token));
setLoading(false);
SplashScreen.hideAsync();
try {
const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
if (!token) {
dispatch(setIsLoggedIn(false));
return;
}
// Token exists, now verify it by calling getUserDetails
// await dispatch(getUserDetails()).unwrap();
// If it reaches here, the token is valid
dispatch(setIsLoggedIn(true));
} catch (error) {
// getUserDetails failed => token is invalid
dispatch(setIsLoggedIn(false));
await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); // optionally clean up
} finally {
setLoading(false);
SplashScreen.hideAsync();
}
};
loadAuth();
}, []);

183
app/user/edit_name.tsx Normal file
View File

@ -0,0 +1,183 @@
import React from "react";
import {
View,
Text,
TextInput,
StyleSheet,
TouchableOpacity,
KeyboardAvoidingView,
Platform,
Keyboard,
TouchableWithoutFeedback,
} from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store";
import { Formik } from "formik";
import * as Yup from "yup";
import api from "@/services/axiosClient";
import { BASE_URL } from "@/constants/config";
import { setUsername } from "@/store/userSlice";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { router } from "expo-router";
import { useSnackbar } from "@/contexts/Snackbar";
export default function EditName() {
const { data } = useSelector((state: RootState) => state.user);
const originalName = data?.name || "";
const dispatch = useDispatch();
const insets = useSafeAreaInsets();
const nameSchema = Yup.object().shape({
name: Yup.string().required("Name is required"),
});
const { showSnackbar } = useSnackbar();
const handleSave = async (values: { name: string }) => {
try {
await api.put(`${BASE_URL}/api/v1/update-user-information`, {
name: values.name,
mobile: data?.mobile,
});
dispatch(setUsername(values.name));
showSnackbar("Name updated successfully", "success");
router.back();
} catch (error) {
console.error("Error updating name:", error);
}
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.inner}>
<Formik
initialValues={{ name: originalName }}
validationSchema={nameSchema}
onSubmit={handleSave}
enableReinitialize
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
touched,
errors,
}) => {
const hasChanged = values.name !== originalName;
const hasError = !!errors.name;
return (
<View style={styles.formContainer}>
<View style={styles.inputContainer}>
<Text style={styles.label}>Enter Name</Text>
<TextInput
style={[
styles.input,
{
borderColor:
touched.name && errors.name ? "#D51D10" : "#D8DDE7",
},
]}
value={values.name}
onChangeText={handleChange("name")}
onBlur={handleBlur("name")}
placeholder="Enter your name"
placeholderTextColor="#949CAC"
/>
{touched.name && errors.name && (
<Text style={styles.error}>{errors.name}</Text>
)}
</View>
<TouchableOpacity
onPress={handleSubmit as unknown as () => void}
style={[
styles.button,
hasChanged && !hasError
? styles.buttonEnabled
: styles.buttonDisabled,
{ marginBottom: insets.bottom },
]}
disabled={!hasChanged || hasError}
>
<Text style={styles.buttonText}>Save</Text>
</TouchableOpacity>
</View>
);
}}
</Formik>
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#F3F5F8",
paddingHorizontal: 16,
},
inner: {
flex: 1,
justifyContent: "space-between",
paddingVertical: 24,
},
formContainer: {
flex: 1,
justifyContent: "space-between",
},
inputContainer: {
marginBottom: 24,
},
label: {
fontSize: 14,
marginBottom: 8,
color: "#252A34",
fontFamily: "Inter",
lineHeight: 20,
},
input: {
backgroundColor: "#ffffff",
borderRadius: 4,
borderWidth: 1,
height: 40,
paddingHorizontal: 8,
fontSize: 14,
fontFamily: "Inter",
fontWeight: "500",
lineHeight: 20,
},
error: {
marginTop: 8,
color: "#D51D10",
fontSize: 12,
fontFamily: "Inter",
fontWeight: "bold",
lineHeight: 20,
},
button: {
height: 48,
borderRadius: 4,
alignItems: "center",
justifyContent: "center",
width: "100%",
paddingHorizontal: 16,
},
buttonEnabled: {
backgroundColor: "#008761",
},
buttonDisabled: {
backgroundColor: "#B0B7C5", // from Figma: rgba(176, 183, 197)
},
buttonText: {
color: "#FCFCFC",
fontSize: 14,
fontFamily: "Inter",
fontWeight: "bold",
lineHeight: 20,
},
});

View File

@ -9,6 +9,10 @@ import {
} from "react-native";
import { MaterialIcons } from "@expo/vector-icons";
import LanguageModal from "@/components/Profile/LangaugeModal";
import { useSelector } from "react-redux";
import { RootState } from "@/store";
import EditIcon from "../../assets/icons/edit.svg";
import { router } from "expo-router";
export default function ProfileScreen() {
const [isLangaugeModalVisible, setLanguageModalVisible] =
@ -17,6 +21,12 @@ export default function ProfileScreen() {
const toggleLanguageModal = () => {
setLanguageModalVisible(!isLangaugeModalVisible);
};
const { data } = useSelector((state: RootState) => state.user);
const userName = data?.name || "User";
const mobileNumber = data?.mobile || "Not provided";
return (
<>
<ScrollView contentContainerStyle={styles.scrollContent}>
@ -26,16 +36,20 @@ export default function ProfileScreen() {
style={styles.avatar}
/>
<TouchableOpacity style={styles.editAvatar}>
<MaterialIcons name="edit" size={20} color="#FDFDFD" />
<EditIcon />
</TouchableOpacity>
</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>
<Text style={styles.value}>{userName}</Text>
</View>
<TouchableOpacity>
<TouchableOpacity
onPress={() => {
router.push("/user/edit_name");
}}
>
<MaterialIcons name="edit" size={20} color="#555C70" />
</TouchableOpacity>
</View>
@ -43,7 +57,7 @@ export default function ProfileScreen() {
<View style={styles.row}>
<View style={styles.textGroup}>
<Text style={styles.label}>Mobile Number</Text>
<Text style={styles.value}>9876543210</Text>
<Text style={styles.value}>{mobileNumber}</Text>
</View>
</View>
</View>
@ -113,7 +127,7 @@ const styles = StyleSheet.create({
editAvatar: {
position: "absolute",
bottom: 0,
right: 105,
right: "35%",
width: 40,
height: 40,
backgroundColor: "#008866",

8
assets/icons/edit.svg Normal file
View File

@ -0,0 +1,8 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_14_4327" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
<rect width="20" height="20" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_14_4327)">
<path d="M4.16667 15.8333H5.35417L13.5 7.6875L12.3125 6.5L4.16667 14.6458V15.8333ZM3.33333 17.5C3.09722 17.5 2.89931 17.4201 2.73958 17.2604C2.57986 17.1007 2.5 16.9028 2.5 16.6667V14.6458C2.5 14.4236 2.54167 14.2118 2.625 14.0104C2.70833 13.809 2.82639 13.6319 2.97917 13.4792L13.5 2.97917C13.6667 2.82639 13.8507 2.70833 14.0521 2.625C14.2535 2.54167 14.4653 2.5 14.6875 2.5C14.9097 2.5 15.125 2.54167 15.3333 2.625C15.5417 2.70833 15.7222 2.83333 15.875 3L17.0208 4.16667C17.1875 4.31944 17.309 4.5 17.3854 4.70833C17.4618 4.91667 17.5 5.125 17.5 5.33333C17.5 5.55556 17.4618 5.76736 17.3854 5.96875C17.309 6.17014 17.1875 6.35417 17.0208 6.52083L6.52083 17.0208C6.36806 17.1736 6.19097 17.2917 5.98958 17.375C5.78819 17.4583 5.57639 17.5 5.35417 17.5H3.33333ZM12.8958 7.10417L12.3125 6.5L13.5 7.6875L12.8958 7.10417Z" fill="#FCFCFC"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -199,6 +199,7 @@ export const {
clearSendOTPError,
clearVerifyOTPError,
clearAllErrors,
setIsLoggedIn,
} = authSlice.actions;
export default authSlice.reducer;

View File

@ -1,10 +1,12 @@
import { combineReducers } from "@reduxjs/toolkit";
import authreducer from "./authSlice";
import telemetryReducer from "./telemetrySlice";
import userReducer from "./userSlice";
const rootReducer = combineReducers({
auth: authreducer,
telemetry: telemetryReducer,
user: userReducer,
});
export default rootReducer;

View File

@ -36,8 +36,10 @@ export const getUserDetails = createAsyncThunk<UserData>(
"user/getUserDetails",
async (_, { rejectWithValue }) => {
try {
console.log("Fetching user details from API...");
const response = await api.get(`${BASE_URL}/api/v1/get-user-details`);
if (response.data.success) {
console.log("User details fetched successfully:", response.data.data);
return response.data.data;
} else {
return rejectWithValue("Failed to fetch user data");
@ -61,6 +63,13 @@ const userSlice = createSlice({
state.error = null;
state.loading = false;
},
setUsername: (state, action: PayloadAction<string>) => {
if (state.data) {
state.data.name = action.payload;
} else {
console.warn("Cannot set username, user data is not loaded");
}
},
},
extraReducers: (builder) => {
builder
@ -82,5 +91,5 @@ const userSlice = createSlice({
},
});
export const { clearUser } = userSlice.actions;
export const { clearUser, setUsername } = userSlice.actions;
export default userSlice.reducer;