//packgae.json use npm install
{
"name": "myStore",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native/new-app-screen": "0.81.4",
"@react-navigation/bottom-tabs": "^7.4.7",
"@react-navigation/drawer": "^7.5.8",
"@react-navigation/native": "^7.1.17",
"@react-navigation/stack": "^7.4.8",
"express": "^5.1.0",
"formik": "^2.4.6",
"react": "19.1.0",
"react-native": "0.81.4",
"react-native-dropdown-picker": "^5.4.6",
"react-native-gesture-handler": "^2.28.0",
"react-native-image-picker": "^8.2.1",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "^4.1.2",
"react-native-safe-area-context": "^5.6.1",
"react-native-screens": "^4.16.0",
"react-native-vector-icons": "^10.3.0",
"react-native-worklets": "^0.6.0",
"yup": "^1.7.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli": "20.0.0",
"@react-native-community/cli-platform-android": "20.0.0",
"@react-native-community/cli-platform-ios": "20.0.0",
"@react-native/babel-preset": "0.81.4",
"@react-native/eslint-config": "0.81.4",
"@react-native/metro-config": "0.81.4",
"@react-native/typescript-config": "0.81.4",
"@types/jest": "^29.5.13",
"@types/react": "^19.1.0",
"@types/react-test-renderer": "^19.1.0",
"eslint": "^8.19.0",
"jest": "^29.6.3",
"prettier": "2.8.8",
"react-test-renderer": "19.1.0",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=20"
}
}
//
//index.js
///**
* @format
*/
import { AppRegistry } from 'react-native';
import AppNavigator from './AppNavigator';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => AppNavigator);
//
//
//
// AppNavigator.js
import { LogBox } from "react-native";
import React, { useContext } from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createDrawerNavigator } from "@react-navigation/drawer";
import Icon from "react-native-vector-icons/Ionicons";
import { AuthProvider, AuthContext } from "./src/AuthContext";
import { CartProvider } from "./src/CartContext";
import App from "./App";
import Detail from "./Details";
import Cart from "./Cart";
import Profile from "./Profile";
import LoginScreen from "./src/screens/LoginScreen";
import SignupScreen from "./src/screens/SignupScreen";
LogBox.ignoreLogs(["SafeAreaView has been deprecated"]);
const Stack = createStackNavigator();
const Tab = createBottomTabNavigator();
const Drawer = createDrawerNavigator();
function HomeStack() {
return (
<Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: "orange" } }}>
<Stack.Screen
name="Infinity Store"
component={App}
options={{
headerTitleStyle: {
fontWeight: "bold",
fontStyle: "italic",
fontSize: 30,
},
}}
/>
<Stack.Screen name="Detail" component={Detail} />
</Stack.Navigator>
);
}
function MyTabs({ navigation }) {
const { cartItems } = React.useContext(require("./src/CartContext").CartContext);
const totalItems = cartItems.length;
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ color, size }) => {
let iconName;
if (route.name === "Home") iconName = "home";
else if (route.name === "Cart") iconName = "cart";
else if (route.name === "More") iconName = "menu";
else if (route.name === "Profile") iconName = "person";
return <Icon name={iconName} size={size} color={color} />;
},
headerStyle: { backgroundColor: "orange" },
tabBarStyle: { backgroundColor: "black" },
tabBarActiveTintColor: "orange",
tabBarInactiveTintColor: "white",
})}
>
<Tab.Screen name="Home" component={HomeStack} options={{ headerShown: false }} />
<Tab.Screen
name="Cart"
component={Cart}
options={{
tabBarBadge: totalItems > 0 ? totalItems : null,
tabBarBadgeStyle: { backgroundColor: "red", color: "white" },
}}
/>
<Tab.Screen name="Profile" component={Profile} />
{/* ✅ “More” tab now clean + opens drawer */}
<Tab.Screen name="More" component={() => null} listeners={{
tabPress: (e) => {
e.preventDefault();
navigation.openDrawer();
}
}} />
</Tab.Navigator>
);
}
function MyDrawer() {
return (
<Drawer.Navigator screenOptions={{ drawerType: "front" }}>
<Drawer.Screen
name="MainTabs"
component={MyTabs}
options={{ headerShown: false, title: "Menu" }}
/>
<Drawer.Screen
name="CartDrawer"
component={MyTabs}
options={{ headerShown: false, drawerLabel: "Cart" }}
listeners={({ navigation }) => ({
drawerItemPress: (e) => {
e.preventDefault();
navigation.navigate("MainTabs", { screen: "Cart" });
},
})}
/>
<Drawer.Screen
name="ProfileDrawer"
component={MyTabs}
options={{ headerShown: false, drawerLabel: "Profile" }}
listeners={({ navigation }) => ({
drawerItemPress: (e) => {
e.preventDefault();
navigation.navigate("MainTabs", { screen: "Profile" });
},
})}
/>
</Drawer.Navigator>
);
}
// Auth stack (Login / Signup)
function AuthStack() {
return (
<Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: "orange" } }}>
<Stack.Screen name="Login" component={LoginScreen} options={{ headerShown: false }} />
<Stack.Screen name="Signup" component={SignupScreen} options={{ headerShown: false }} />
</Stack.Navigator>
);
}
// Root Navigation
function RootNavigation() {
const { user, loading } = useContext(AuthContext);
if (loading) return null;
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{user ? (
<Stack.Screen name="MyDrawer" component={MyDrawer} />
) : (
<Stack.Screen name="Auth" component={AuthStack} />
)}
</Stack.Navigator>
);
}
export default function AppNavigator() {
return (
<AuthProvider>
<CartProvider>
<NavigationContainer>
<RootNavigation />
</NavigationContainer>
</CartProvider>
</AuthProvider>
);
}
//
//
//App.js
import React, { useState, useEffect } from "react";
import {
View,
Text,
Button,
FlatList,
StyleSheet,
Image,
TextInput,
TouchableOpacity,
} from "react-native";
import DropDownPicker from "react-native-dropdown-picker";
const Home = ({ navigation }) => {
// State for products and search
const [product, setProduct] = useState([]);
const [search, setSearch] = useState("");
// Category dropdown
const [categoryOpen, setCategoryOpen] = useState(false);
const [selectedCategory, setSelectedCategory] = useState("");
const [categoryItems, setCategoryItems] = useState([
{ label: "All Category", value: "" },
{ label: "Electronics", value: "electronics" },
{ label: "Jewelery", value: "jewelery" },
{ label: "Men's clothing", value: "men's clothing" },
{ label: "Women's clothing", value: "women's clothing" },
]);
// Price dropdown
const [priceOpen, setPriceOpen] = useState(false);
const [selectedPriceRange, setSelectedPriceRange] = useState("");
const [priceItems, setPriceItems] = useState([
{ label: "All Prices", value: "" },
{ label: "0 - 100", value: "0-100" },
{ label: "100 - 200", value: "100-200" },
{ label: "200 - 500", value: "200-500" },
{ label: "500 - 1000", value: "500-1000" },
]);
// Fetch products from API
useEffect(() => {
fetch("https://fakestoreapi.com/products")
.then((res) => res.json())
.then((data) => setProduct(data))
.catch((err) => console.error(err));
}, []);
// Filter function (search + category + price)
const filter = () => {
const term = search.trim().toLowerCase();
return product.filter((p) => {
const matchTerm = !term || p.title.toLowerCase().includes(term);
const matchCategory =
!selectedCategory ||
p.category.toLowerCase() === selectedCategory.toLowerCase();
let matchPrice = true;
if (selectedPriceRange) {
const [min, max] = selectedPriceRange.split("-").map(Number);
matchPrice = p.price >= min && p.price <= max;
}
return matchTerm && matchCategory && matchPrice;
});
};
// Render UI
const filteredProducts = filter();
return (
<View style={styles.container}>
<View style={{ backgroundColor: "orange" }}>
{/* Search Box */}
<View style={styles.searchBox}>
<TextInput
style={styles.searchInput}
placeholder="Search..."
placeholderTextColor="black"
onChangeText={setSearch}
value={search}
/>
</View>
{/* Dropdowns */}
<View style={styles.dropdownRow}>
{/* Category Dropdown */}
<View style={styles.dropdownWrapper}>
<DropDownPicker
open={categoryOpen}
value={selectedCategory}
items={categoryItems}
setOpen={setCategoryOpen}
setValue={setSelectedCategory}
setItems={setCategoryItems}
style={styles.dropdown}
dropDownContainerStyle={styles.dropdownContainer}
textStyle={{ color: "black" }}
/>
</View>
{/* Price Dropdown */}
<View style={styles.dropdownWrapper}>
<DropDownPicker
open={priceOpen}
value={selectedPriceRange}
items={priceItems}
setOpen={setPriceOpen}
setValue={setSelectedPriceRange}
setItems={setPriceItems}
style={styles.dropdown}
dropDownContainerStyle={styles.dropdownContainer}
textStyle={{ color: "black" }}
/>
</View>
</View>
</View>
{/* Product List */}
{filteredProducts.length === 0 ? (
<View style={styles.notFoundContainer}>
<Text style={styles.notFoundText}>No items found </Text>
</View>
) : (
<FlatList
data={filteredProducts}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() =>
navigation.navigate("Detail", { product: item })
}
activeOpacity={0.95}
>
<View style={styles.card}>
<Image source={{ uri: item.image }} style={styles.image} />
<View style={{ flexShrink: 1 }}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.price}>Price: ${item.price}</Text>
<Text style={styles.category}>Category: {item.category}</Text>
<Text style={styles.rating}>⭐ {item.rating.rate}</Text>
<Button
color="orange"
title="Details"
onPress={() =>
navigation.navigate("Detail", { product: item })
}
/>
</View>
</View>
</TouchableOpacity>
)}
/>
)}
</View>
);
};
export default Home;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f2f2f2",
paddingTop: 0,
},
// Search Box
searchBox: {
borderWidth: 2,
borderColor: "orange",
borderRadius: 20,
marginHorizontal: 10,
marginBottom: 10,
marginTop: 10,
},
searchInput: {
color: "black",
padding: 10,
backgroundColor: "white",
borderRadius: 10,
},
// Dropdowns
dropdownRow: {
flexDirection: "row",
marginHorizontal: 10,
marginBottom: 10,
},
dropdownWrapper: {
flex: 1,
marginHorizontal: 5,
},
dropdown: {
backgroundColor: "white",
borderColor: "orange",
borderWidth: 2,
borderRadius: 20,
},
dropdownContainer: {
backgroundColor: "white",
borderColor: "orange",
},
// Product Card
card: {
backgroundColor: "white",
marginBottom: 10,
padding: 30,
borderRadius: 5,
borderWidth: 2,
borderColor: "orange",
marginHorizontal: 5,
flexDirection: "row",
alignItems: "center",
},
image: {
height: 180,
width: 180,
marginRight: 10,
resizeMode: "contain",
},
title: {
fontSize: 18,
fontWeight: "500",
textAlign: "center",
marginBottom: 10,
},
price: {
fontSize: 16,
color: "green",
marginBottom: 10,
},
category: {
fontSize: 14,
fontStyle: "italic",
marginBottom: 10,
},
rating: {
fontSize: 14,
color: "orange",
},
// No items found
notFoundContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
marginTop: 50,
},
notFoundText: {
fontSize: 18,
fontWeight: "bold",
color: "gray",
},
});
//
//babel.config.js
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
'react-native-reanimated/plugin', // must be last
],
};
//
//
//Cart.js
import { View, Text, FlatList, StyleSheet, Image, Button, TouchableOpacity, Modal } from "react-native";
import { useContext, useState } from "react";
import { CartContext } from "./src/CartContext";
const Cart = () => {
const { cartItems, increaseQuantity, decreaseQuantity, removeFromCart } =
useContext(CartContext);
const [modalVisible, setModalVisible] = useState(false);
// calculate total price including quantity
const totalPrice = cartItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const handlePayNow = () => {
if (cartItems.length === 0) {
return; // do nothing if cart is empty
}
setModalVisible(true); // show modal on pay now
};
const closeModal = () => {
setModalVisible(false);
};
return (
<View style={styles.container}>
{cartItems.length === 0 ? (
<Text style={styles.empty}>Your cart is empty 🛒</Text>
) : (
<>
{/* Top view: total price + pay now */}
<View style={styles.topContainer}>
<Text style={styles.totalText}>Total: ${totalPrice.toFixed(2)}</Text>
<TouchableOpacity style={styles.payButton} onPress={handlePayNow}>
<Text style={styles.payText}>Pay Now</Text>
</TouchableOpacity>
</View>
<FlatList
data={cartItems}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<View style={styles.card}>
<Image source={{ uri: item.image }} style={styles.image} />
<View style={{ flex: 1 }}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.price}>${item.price}</Text>
<Text style={styles.quantity}>Qty: {item.quantity}</Text>
{/* Buttons Row */}
<View style={styles.buttonsRow}>
<View style={{ flex: 1, marginHorizontal: 5 }}>
<Button
color="orange"
title="+"
onPress={() => increaseQuantity(item.id)}
/>
</View>
<View style={{ flex: 1, marginHorizontal: 5 }}>
<Button
color="orange"
title="-"
onPress={() => decreaseQuantity(item.id)}
/>
</View>
<View style={{ flex: 1, marginHorizontal: 5 }}>
<Button
title="Remove"
color="red"
onPress={() => removeFromCart(item.id)}
/>
</View>
</View>
</View>
</View>
)}
/>
{/* Custom Modal for order success */}
<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={closeModal}
>
<View style={styles.modalBackground}>
<View style={styles.modalContainer}>
<Text style={styles.modalTitle}>Order Placed!</Text>
<Text style={styles.modalMessage}>
Your order has been placed successfully.
</Text>
<TouchableOpacity style={styles.modalButton} onPress={closeModal}>
<Text style={styles.modalButtonText}>OK</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</>
)}
</View>
);
};
export default Cart;
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 15,
backgroundColor: "#f2f2f2",
},
topContainer: {
backgroundColor: "white",
padding: 15,
borderRadius: 10,
borderWidth: 2,
borderColor: "orange",
marginBottom: 15,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
totalText: {
fontSize: 18,
fontWeight: "bold",
color: "black",
},
payButton: {
backgroundColor: "green",
paddingVertical: 8,
paddingHorizontal: 15,
borderRadius: 8,
},
payText: {
color: "white",
fontWeight: "bold",
fontSize: 16,
},
empty: {
fontSize: 18,
textAlign: "center",
marginTop: 50,
fontWeight: "bold",
color: "gray",
},
card: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "white",
borderRadius: 15,
padding: 10,
marginBottom: 10,
borderWidth: 2,
borderColor: "orange",
},
image: {
height: 80,
width: 80,
resizeMode: "contain",
marginRight: 10,
},
title: {
fontSize: 16,
fontWeight: "600",
},
price: {
fontSize: 14,
color: "green",
marginTop: 5,
},
quantity: {
fontSize: 14,
color: "orange",
marginTop: 5,
},
buttonsRow: {
flexDirection: "row", // buttons side by side
justifyContent: "space-between",
marginTop: 10,
},
// Modal styles
modalBackground: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0,0,0,0.5)",
},
modalContainer: {
width: "80%",
backgroundColor: "white",
borderRadius: 15,
padding: 20,
alignItems: "center",
},
modalTitle: {
fontSize: 20,
fontWeight: "bold",
marginBottom: 10,
},
modalMessage: {
fontSize: 16,
textAlign: "center",
marginBottom: 20,
},
modalButton: {
backgroundColor: "orange",
paddingVertical: 10,
paddingHorizontal: 25,
borderRadius: 8,
},
modalButtonText: {
color: "white",
fontWeight: "bold",
fontSize: 16,
},
});
//
//
//db.json
{
"users": [
{
"id": "1",
"name": "Demo User",
"email": "demo@example.com",
"password": "123456",
"cart": [
{
"id": 1,
"title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
"price": 109.95,
"description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday",
"category": "men's clothing",
"image": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_t.png",
"rating": {
"rate": 3.9,
"count": 120
},
"quantity": 1
},
{
"id": 2,
"title": "Mens Casual Premium Slim Fit T-Shirts ",
"price": 22.3,
"description": "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.",
"category": "men's clothing",
"image": "https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_t.png",
"rating": {
"rate": 4.1,
"count": 259
},
"quantity": 1
}
],
"avatar": null
}]}
//
//
//Details.js
import { View, Text, StyleSheet, Image, Button, Modal, TouchableOpacity,ScrollView } from "react-native";
import { useContext, useState } from "react";
import { CartContext } from "./src/CartContext";
const Detail = ({ route }) => {
const { product } = route.params;
const { addToCart } = useContext(CartContext);
const [modalVisible, setModalVisible] = useState(false);
const handleAddToCart = () => {
addToCart(product);
setModalVisible(true); // ✅ Show modal
};
return (
<View style={styles.container}>
<ScrollView>
<View style={styles.card}>
<Image source={{ uri: product.image }} style={styles.image} />
<Text style={styles.title}>{product.title}</Text>
<Text style={styles.price}>Price: ${product.price}</Text>
<Text style={styles.category}>Category: {product.category}</Text>
<Text style={styles.description}>{product.description}</Text>
<Text style={styles.rating}>
⭐ {product.rating.rate} ({product.rating.count} reviews)
</Text>
<Button color="orange" title="Add to Cart" onPress={handleAddToCart} />
</View>
</ScrollView>
{/* ✅ Success Modal */}
<Modal
visible={modalVisible}
transparent
animationType="fade"
onRequestClose={() => setModalVisible(false)}
>
<View style={styles.modalBackground}>
<View style={styles.modalContainer}>
<Text style={styles.modalText}>✅ Successfully added to cart!</Text>
<TouchableOpacity
style={styles.modalButton}
onPress={() => setModalVisible(false)}
>
<Text style={styles.modalButtonText}>OK</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</View>
);
};
export default Detail;
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 15,
backgroundColor: "#f2f2f2",
},
card: {
backgroundColor: "white",
borderRadius: 15,
padding: 20,
elevation: 4,
alignItems: "center",
borderWidth: 3,
borderColor: "orange",
},
image: {
height: 180,
width: 180,
marginBottom: 15,
resizeMode: "contain",
},
title: {
fontSize: 18,
fontWeight: "700",
textAlign: "center",
marginBottom: 10,
},
price: {
fontSize: 16,
color: "green",
marginBottom: 10,
},
category: {
fontSize: 14,
fontStyle: "italic",
marginBottom: 10,
},
description: {
fontSize: 14,
marginBottom: 10,
textAlign: "center",
},
rating: {
fontSize: 14,
color: "orange",
marginBottom: 10,
},
modalBackground: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0,0,0,0.5)",
},
modalContainer: {
backgroundColor: "white",
padding: 25,
borderRadius: 15,
alignItems: "center",
elevation: 5,
width: "80%",
},
modalText: {
fontSize: 16,
fontWeight: "bold",
marginBottom: 15,
textAlign: "center",
},
modalButton: {
backgroundColor: "orange",
paddingHorizontal: 25,
paddingVertical: 10,
borderRadius: 10,
},
modalButtonText: {
color: "white",
fontWeight: "bold",
},
});
//
//
//
//EditProfileModal.js
import React, { useState, useEffect } from "react";
import {
View,
Text,
Modal,
StyleSheet,
TouchableOpacity,
TextInput,
Image,
ActivityIndicator,
Platform,
PermissionsAndroid,
} from "react-native";
import { launchImageLibrary, launchCamera } from "react-native-image-picker";
import AsyncStorage from "@react-native-async-storage/async-storage";
export default function EditProfileModal({
visible,
onClose,
user,
apiHost,
onSaved,
}) {
const [name, setName] = useState(user?.name || "");
const [email, setEmail] = useState(user?.email || "");
const [password, setPassword] = useState(user?.password || "");
const [avatarPreview, setAvatarPreview] = useState(user?.avatar || null);
const [saving, setSaving] = useState(false);
const [infoMessage, setInfoMessage] = useState(null);
const [showPassword, setShowPassword] = useState(false);
useEffect(() => {
setName(user?.name || "");
setEmail(user?.email || "");
setPassword(user?.password || "");
setAvatarPreview(user?.avatar || null);
setInfoMessage(null);
setShowPassword(false);
}, [user, visible]);
const USERS_ENDPOINT = `${apiHost}/users`;
const showInfo = (msg) => {
setInfoMessage(msg);
setTimeout(() => setInfoMessage(null), 1500);
};
const validate = () => {
if (!name.trim()) {
showInfo("Name is required");
return false;
}
if (!email.trim() || !/^\S+@\S+\.\S+$/.test(email)) {
showInfo("Enter a valid email");
return false;
}
if (password && password.length < 4) {
showInfo("Password at least 4 chars");
return false;
}
return true;
};
const patchUser = async (payload) => {
const id = user.id;
const url = `${USERS_ENDPOINT}/${encodeURIComponent(id)}`;
const resp = await fetch(url, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!resp.ok) {
const text = await resp.text().catch(() => null);
throw new Error(`Failed (${resp.status}) ${text || ""}`);
}
return resp.json();
};
const persistUserLocally = async (updatedUser) => {
try {
const stored = await AsyncStorage.getItem("@user_profile");
if (!stored) {
await AsyncStorage.setItem("@user_profile", JSON.stringify(updatedUser));
return;
}
const parsed = JSON.parse(stored);
if (parsed && String(parsed.id) === String(updatedUser.id)) {
await AsyncStorage.setItem("@user_profile", JSON.stringify({ ...parsed, ...updatedUser }));
} else {
await AsyncStorage.setItem("@user_profile", JSON.stringify(updatedUser));
}
} catch (e) {
console.warn("Persist user local failed", e);
}
};
const handleSave = async () => {
if (!validate()) return;
setSaving(true);
try {
const payload = {};
if (name !== user.name) payload.name = name.trim();
if (email !== user.email) payload.email = email.trim();
if (password !== user.password) payload.password = password;
// avatarPreview may be null (remove) or a data URI
if (avatarPreview !== user.avatar) payload.avatar = avatarPreview === null ? null : avatarPreview;
if (Object.keys(payload).length === 0) {
showInfo("No changes");
onClose();
return;
}
const updated = await patchUser(payload);
await persistUserLocally(updated);
showInfo("Saved");
onSaved && onSaved(updated);
setTimeout(onClose, 700);
} catch (err) {
console.error("Save profile error:", err);
showInfo("Save failed");
} finally {
setSaving(false);
}
};
const pickImage = async () => {
try {
const res = await launchImageLibrary({
mediaType: "photo",
includeBase64: true,
quality: 0.6,
selectionLimit: 1,
});
if (res.didCancel) return;
if (res.errorCode) {
showInfo("Image error");
return;
}
const asset = res.assets && res.assets[0];
if (asset && asset.base64) {
const mime = asset.type || "image/jpeg";
const dataUri = `data:${mime};base64,${asset.base64}`;
setAvatarPreview(dataUri); // update preview immediately
} else showInfo("Unsupported");
} catch (err) {
console.error("pickImage error", err);
showInfo("Pick failed");
}
};
const requestCameraPermissionAndroid = async () => {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.CAMERA,
{
title: "Camera Permission",
message: "App needs camera access to take profile photos.",
buttonPositive: "OK",
buttonNegative: "Cancel",
}
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch (err) {
console.warn("Camera permission error", err);
return false;
}
};
const takePhoto = async () => {
try {
if (Platform.OS === "android") {
const ok = await requestCameraPermissionAndroid();
if (!ok) {
showInfo("Camera permission denied");
return;
}
}
const res = await launchCamera({
mediaType: "photo",
includeBase64: true,
quality: 0.6,
saveToPhotos: false,
});
if (res.didCancel) return;
if (res.errorCode) {
showInfo("Camera error");
return;
}
const asset = res.assets && res.assets[0];
if (asset && asset.base64) {
const mime = asset.type || "image/jpeg";
const dataUri = `data:${mime};base64,${asset.base64}`;
setAvatarPreview(dataUri);
} else showInfo("Unsupported");
} catch (err) {
console.error("takePhoto error", err);
showInfo("Camera failed");
}
};
const removePhoto = () => {
setAvatarPreview(null);
showInfo("Photo removed");
};
return (
<Modal visible={visible} animationType="slide" transparent>
<View style={styles.modalBackdrop}>
<View style={styles.modalCard}>
<Text style={styles.title}>Edit Profile</Text>
<View style={styles.avatarRow}>
{avatarPreview ? (
<Image source={{ uri: avatarPreview }} style={styles.avatarLarge} />
) : (
<View style={[styles.avatarLarge, styles.avatarPlaceholder]}>
<Text style={{ color: "#666" }}>No photo</Text>
</View>
)}
</View>
<View style={styles.rowButtons}>
<TouchableOpacity style={styles.smallBtn} onPress={pickImage}>
<Text style={styles.smallBtnText}>Pick</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.smallBtn, { backgroundColor: "#1c7c54" }]} onPress={takePhoto}>
<Text style={styles.smallBtnText}>Camera</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.smallBtn, { backgroundColor: "#d9534f" }]} onPress={removePhoto}>
<Text style={styles.smallBtnText}>Remove</Text>
</TouchableOpacity>
</View>
<TextInput style={styles.input} value={name} onChangeText={setName} placeholder="Name" placeholderTextColor="#888" />
<TextInput
style={styles.input}
value={email}
onChangeText={setEmail}
placeholder="Email"
keyboardType="email-address"
placeholderTextColor="#888"
/>
{/* Password field with eye toggle and black text */}
<View style={styles.passwordRow}>
<TextInput
style={[styles.input, { flex: 1, color: "#000" }]}
value={password}
onChangeText={setPassword}
placeholder="Password"
secureTextEntry={!showPassword}
placeholderTextColor="#888"
/>
<TouchableOpacity
onPress={() => setShowPassword((s) => !s)}
style={styles.eyeBtn}
accessibilityLabel="Toggle password visibility"
>
<Text style={styles.eyeBtnText}>{showPassword ? "Hide" : "Show"}</Text>
</TouchableOpacity>
</View>
<View style={{ flexDirection: "row", justifyContent: "space-between", marginTop: 10 }}>
<TouchableOpacity style={[styles.actionBtn, { backgroundColor: "#aaa" }]} onPress={onClose} disabled={saving}>
<Text style={styles.actionBtnText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionBtn, { backgroundColor: "#0a84ff" }]} onPress={handleSave} disabled={saving}>
{saving ? <ActivityIndicator color="#fff" /> : <Text style={styles.actionBtnText}>Save</Text>}
</TouchableOpacity>
</View>
{infoMessage && (
<View style={styles.infoBox}>
<Text style={{ color: "white" }}>{infoMessage}</Text>
</View>
)}
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
modalBackdrop: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.45)",
justifyContent: "center",
padding: 16,
},
modalCard: {
backgroundColor: "white",
borderRadius: 12,
padding: 16,
elevation: 6,
},
title: { fontSize: 18, fontWeight: "700", marginBottom: 8, textAlign: "center" },
avatarLarge: { width: 110, height: 110, borderRadius: 60, marginBottom: 8 },
avatarPlaceholder: { backgroundColor: "#f0f0f0", alignItems: "center", justifyContent: "center" },
avatarRow: { alignItems: "center", marginBottom: 8 },
rowButtons: { flexDirection: "row", justifyContent: "space-between", marginBottom: 8 },
smallBtn: { flex: 1, padding: 8, marginHorizontal: 4, backgroundColor: "#0a84ff", borderRadius: 6, alignItems: "center" },
smallBtnText: { color: "white", fontWeight: "600" },
input: { borderWidth: 1, borderColor: "#ddd", borderRadius: 8, padding: 10, marginTop: 8, color: "#000" },
passwordRow: { flexDirection: "row", alignItems: "center", marginTop: 8 },
eyeBtn: { marginLeft: 8, paddingHorizontal: 10, paddingVertical: 8, backgroundColor: "#eee", borderRadius: 8 },
eyeBtnText: { color: "#333", fontWeight: "700" },
actionBtn: { flex: 1, marginHorizontal: 6, padding: 12, borderRadius: 8, alignItems: "center" },
actionBtnText: { color: "white", fontWeight: "700" },
infoBox: { position: "absolute", bottom: 12, left: 12, right: 12, backgroundColor: "#333", padding: 8, borderRadius: 8, alignItems: "center" },
});
//
//
//Profile.js
import React, { useContext, useState, useEffect } from "react";
import {
View,
Text,
TouchableOpacity,
Image,
ActivityIndicator,
Modal,
StyleSheet,
} from "react-native";
import { AuthContext } from "./src/AuthContext";
import { CartContext } from "./src/CartContext";
import AsyncStorage from "@react-native-async-storage/async-storage";
import EditProfileModal from "./EditProfileModal";
const API_HOST = "http://192.168.187.112:3000";
const USERS_ENDPOINT = `${API_HOST}/users`;
export default function Profile({ navigation }) {
const { user, signOut } = useContext(AuthContext);
const { clearCart } = useContext(CartContext);
const [showLogoutModal, setShowLogoutModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [localAvatar, setLocalAvatar] = useState(null);
const [savingIndicator, setSavingIndicator] = useState(false);
const [infoMessage, setInfoMessage] = useState(null);
const [localUser, setLocalUser] = useState(user);
useEffect(() => {
setLocalUser(user);
if (user && user.avatar) setLocalAvatar(user.avatar);
}, [user]);
if (!user) {
return (
<View style={styles.centered}>
<Text style={styles.text}>You are not logged in.</Text>
</View>
);
}
const showTempInfo = (msg) => {
setInfoMessage(msg);
setTimeout(() => setInfoMessage(null), 1400);
};
const handleLogout = async () => {
setShowLogoutModal(false);
try {
await signOut(clearCart);
} catch (e) {
console.error("Logout error:", e);
showTempInfo("Logout failed");
}
};
const handleRemovePhoto = async () => {
try {
setSavingIndicator(true);
const url = `${USERS_ENDPOINT}/${encodeURIComponent(user.id)}`;
const resp = await fetch(url, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ avatar: null }),
});
if (!resp.ok) throw new Error("Failed");
const updated = await resp.json();
setLocalAvatar(null);
setLocalUser(updated);
try {
const stored = await AsyncStorage.getItem("@user_profile");
if (stored) {
const j = JSON.parse(stored);
if (j && String(j.id) === String(user.id)) {
await AsyncStorage.setItem("@user_profile", JSON.stringify({ ...j, avatar: null }));
}
} else {
await AsyncStorage.setItem("@user_profile", JSON.stringify(updated));
}
} catch (e) {
console.warn("AsyncStorage update failed", e);
}
showTempInfo("Photo removed");
} catch (err) {
console.error("removePhoto error", err);
showTempInfo("Remove failed");
} finally {
setSavingIndicator(false);
}
};
const handleSavedFromModal = async (updatedUser) => {
if (updatedUser) {
setLocalUser(updatedUser);
if (updatedUser.avatar !== undefined) setLocalAvatar(updatedUser.avatar);
try {
const stored = await AsyncStorage.getItem("@user_profile");
if (stored) {
const j = JSON.parse(stored);
if (j && String(j.id) === String(updatedUser.id)) {
await AsyncStorage.setItem("@user_profile", JSON.stringify({ ...j, ...updatedUser }));
} else {
await AsyncStorage.setItem("@user_profile", JSON.stringify(updatedUser));
}
} else {
await AsyncStorage.setItem("@user_profile", JSON.stringify(updatedUser));
}
} catch (e) {
console.warn("AsyncStorage save fail", e);
}
}
showTempInfo("Profile updated");
};
return (
<View style={styles.container}>
<View style={styles.card}>
<View style={styles.row}>
<View style={styles.avatarWrap}>
{localAvatar ? (
<Image source={{ uri: localAvatar }} style={styles.avatar} />
) : (
<View style={[styles.avatar, styles.avatarPlaceholder]}>
<Text style={{ color: "#666" }}>No photo</Text>
</View>
)}
</View>
<View style={styles.info}>
<Text style={styles.name}>{localUser?.name || user.name}</Text>
<Text style={styles.email}>{localUser?.email || user.email}</Text>
<Text style={styles.email}>Password: {localUser?.password ? "•".repeat(6) : "—"}</Text>
</View>
</View>
<View style={styles.cardButtons}>
<TouchableOpacity style={styles.primaryBtn} onPress={() => setShowEditModal(true)}>
<Text style={styles.primaryBtnText}>Edit profile</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.secondaryBtn, { backgroundColor: "#d9534f" }]} onPress={handleRemovePhoto}>
{savingIndicator ? <ActivityIndicator color="#fff" /> : <Text style={styles.secondaryBtnText}>Remove photo</Text>}
</TouchableOpacity>
</View>
</View>
<View style={styles.footer}>
<TouchableOpacity style={styles.logoutBtn} onPress={() => setShowLogoutModal(true)}>
<Text style={styles.logoutBtnText}>Logout</Text>
</TouchableOpacity>
</View>
<Modal visible={showLogoutModal} transparent animationType="fade" onRequestClose={() => setShowLogoutModal(false)}>
<View style={styles.modalOverlay}>
<View style={styles.modalCard}>
<Text style={styles.modalText}>Are you sure you want to logout?</Text>
<View style={styles.modalButtons}>
<TouchableOpacity style={[styles.modalBtn, { backgroundColor: "gray" }]} onPress={() => setShowLogoutModal(false)}>
<Text style={styles.modalBtnText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.modalBtn, { backgroundColor: "red" }]} onPress={handleLogout}>
<Text style={styles.modalBtnText}>Logout</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
<EditProfileModal visible={showEditModal} onClose={() => setShowEditModal(false)} user={localUser || user} apiHost={API_HOST} onSaved={handleSavedFromModal} />
{infoMessage && (
<View style={styles.infoBox}>
<Text style={{ color: "white" }}>{infoMessage}</Text>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: "#f7f7f7" },
centered: { flex: 1, alignItems: "center", justifyContent: "center" },
card: { backgroundColor: "#fff", borderRadius: 12, padding: 16, elevation: 4 },
row: { flexDirection: "row", alignItems: "center" },
avatarWrap: { marginRight: 12 },
avatar: { width: 90, height: 90, borderRadius: 46 },
avatarPlaceholder: { backgroundColor: "#eee", alignItems: "center", justifyContent: "center" },
info: { flex: 1 },
name: { fontSize: 18, fontWeight: "700" },
email: { fontSize: 14, color: "#666", marginTop: 4 },
cardButtons: { flexDirection: "row", marginTop: 12, justifyContent: "space-between" },
primaryBtn: { backgroundColor: "#0a84ff", padding: 12, borderRadius: 8, alignItems: "center", flex: 1, marginRight: 8 },
primaryBtnText: { color: "white", fontWeight: "700" },
secondaryBtn: { padding: 12, borderRadius: 8, alignItems: "center", justifyContent: "center", width: 120 },
secondaryBtnText: { color: "white", fontWeight: "700" },
footer: { flex: 1, justifyContent: "flex-end", alignItems: "center", marginTop: 18 },
logoutBtn: { backgroundColor: "#d9534f", padding: 14, borderRadius: 10, width: "100%" },
logoutBtnText: { color: "white", fontWeight: "700", textAlign: "center" },
modalOverlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.4)",
justifyContent: "center",
alignItems: "center",
},
modalCard: {
width: "80%",
backgroundColor: "white",
borderRadius: 12,
padding: 20,
alignItems: "center",
elevation: 5,
},
modalText: { fontSize: 18, fontWeight: "600", marginBottom: 20, textAlign: "center" },
modalButtons: { flexDirection: "row", justifyContent: "space-between", width: "100%" },
modalBtn: { flex: 1, padding: 12, borderRadius: 8, marginHorizontal: 5, alignItems: "center" },
modalBtnText: { color: "white", fontWeight: "600" },
infoBox: { position: "absolute", bottom: 16, left: 16, right: 16, backgroundColor: "#333", padding: 8, borderRadius: 8, alignItems: "center" },
text: { fontSize: 16 },
});
//
//
//
//android>app>build.gradle
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
//
//
//adroid.menifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- Add these permissions -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="${usesCleartextTraffic}"
android:supportsRtl="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
//
//
//
// src/AuthContext.js
import React, { createContext, useEffect, useState } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Platform } from "react-native";
export const AuthContext = createContext();
const LOCAL_KEY = "myapp_user";
const CART_KEY = "myapp_cart_local";
const BASE_URL =
Platform.OS === "android" ? "http://192.168.187.112:3000" : "http://localhost:3000";
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [restored, setRestored] = useState(false);
// Restore user ONLY ONCE on app start
useEffect(() => {
let isMounted = true;
const restore = async () => {
try {
const raw = await AsyncStorage.getItem(LOCAL_KEY);
if (raw && isMounted) {
const parsed = JSON.parse(raw);
console.log("Restored user:", parsed.email);
setUser(parsed);
}
} catch (e) {
console.warn("Auth restore failed", e);
} finally {
if (isMounted) {
setLoading(false);
setRestored(true);
}
}
};
restore();
return () => {
isMounted = false;
};
}, []); //
// Save user to AsyncStorage
const saveToStorage = async (userObj) => {
try {
await AsyncStorage.setItem(LOCAL_KEY, JSON.stringify(userObj));
console.log("User saved to storage:", userObj.email);
} catch (e) {
console.warn("Failed to save user", e);
}
};
// SignUp (no auto login)
const signUp = async ({ name, email, password }) => {
const existing = await fetch(`${BASE_URL}/users?email=${encodeURIComponent(email)}`).then(r => r.json());
if (existing.length) throw new Error("User with this email already exists");
const res = await fetch(`${BASE_URL}/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, password, cart: [] }),
}).then(r => r.json());
return res; // just return the created user
};
// SignIn
const signIn = async ({ email, password }) => {
const found = await fetch(`${BASE_URL}/users?email=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`).then(r => r.json());
if (!found.length) throw new Error("Invalid credentials");
const u = found[0];
setUser(u);
await saveToStorage(u);
return u;
};
// SignOut - Clear user state and storage
const signOut = async (clearCartFn) => {
try {
console.log("=== LOGOUT STARTED ===");
// 1. Clear cart first if provided
if (clearCartFn) {
console.log("Clearing cart...");
await clearCartFn();
}
// 2. Clear user state IMMEDIATELY
console.log("Clearing user state...");
setUser(null);
// 3. Clear AsyncStorage
console.log("Clearing AsyncStorage...");
await AsyncStorage.multiRemove([LOCAL_KEY, CART_KEY]);
// 4. Verify it's cleared
const check = await AsyncStorage.getItem(LOCAL_KEY);
console.log("Storage check after clear:", check);
console.log("=== LOGOUT COMPLETED ===");
} catch (e) {
console.error("Sign out failed", e);
}
};
const updateUser = async (updated) => {
// Don't update if user is null (logged out)
if (!user) return;
setUser(updated);
await saveToStorage(updated);
};
return (
<AuthContext.Provider
value={{
user,
loading,
restored,
signIn,
signUp,
signOut,
updateUser,
BASE_URL,
}}
>
{children}
</AuthContext.Provider>
);
};
//
//
// src/CartContext.js
import React, { createContext, useContext, useEffect, useState, useRef } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { AuthContext } from "./AuthContext";
export const CartContext = createContext();
const LOCAL_CART_KEY = "myapp_cart_local";
export const CartProvider = ({ children }) => {
const { user, BASE_URL, updateUser } = useContext(AuthContext);
const [cartItems, setCartItems] = useState([]);
const isClearing = useRef(false);
// Load cart whenever user changes
useEffect(() => {
const init = async () => {
// Skip if we're in the middle of clearing
if (isClearing.current) return;
if (!user) {
// Load local cart for guest users
try {
const raw = await AsyncStorage.getItem(LOCAL_CART_KEY);
if (raw) setCartItems(JSON.parse(raw));
else setCartItems([]);
} catch (e) {
console.warn("Failed to load local cart", e);
setCartItems([]);
}
return;
}
try {
// Load server cart for logged-in users
const res = await fetch(`${BASE_URL}/users/${user.id}`).then(r => r.json());
setCartItems(res.cart || []);
updateUser(res);
} catch (e) {
console.warn("Failed to load server cart", e);
setCartItems([]);
}
};
init();
}, [user?.id]); // Only re-run when user ID changes
const persistCart = async (newCart) => {
setCartItems(newCart);
if (user && !isClearing.current) {
try {
const updatedUser = await fetch(`${BASE_URL}/users/${user.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cart: newCart }),
}).then(r => r.json());
updateUser(updatedUser);
} catch (e) {
console.warn("Failed to sync cart to server", e);
}
} else if (!isClearing.current) {
await AsyncStorage.setItem(LOCAL_CART_KEY, JSON.stringify(newCart));
}
};
const addToCart = (product) => {
setCartItems(prev => {
const existing = prev.find(i => i.id === product.id);
const next = existing
? prev.map(i => i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i)
: [...prev, { ...product, quantity: 1 }];
persistCart(next);
return next;
});
};
const increaseQuantity = (id) => {
setCartItems(prev => {
const next = prev.map(i => i.id === id ? { ...i, quantity: i.quantity + 1 } : i);
persistCart(next);
return next;
});
};
const decreaseQuantity = (id) => {
setCartItems(prev => {
const next = prev
.map(i => i.id === id ? { ...i, quantity: Math.max(i.quantity - 1, 0) } : i)
.filter(i => i.quantity > 0);
persistCart(next);
return next;
});
};
const removeFromCart = (id) => {
setCartItems(prev => {
const next = prev.filter(i => i.id !== id);
persistCart(next);
return next;
});
};
const clearCart = async () => {
isClearing.current = true;
setCartItems([]);
try {
await AsyncStorage.removeItem(LOCAL_CART_KEY);
} catch (e) {
console.warn("Failed to clear cart", e);
}
isClearing.current = false;
};
return (
<CartContext.Provider
value={{
cartItems,
addToCart,
increaseQuantity,
decreaseQuantity,
removeFromCart,
clearCart,
}}
>
{children}
</CartContext.Provider>
);
};
//
//
//
// src/screens/LoginScreen.js
import React, { useContext, useEffect, useState } from "react";
import {
View,
StyleSheet,
Alert,
ImageBackground,
KeyboardAvoidingView,
Platform,
Keyboard,
TouchableWithoutFeedback,
} from "react-native";
import { TextInput, Button, Text } from "react-native-paper";
import { Formik } from "formik";
import * as Yup from "yup";
import { AuthContext } from "../AuthContext";
import Icon from "react-native-vector-icons/MaterialIcons";
export default function LoginScreen({ navigation }) {
const { signIn, user, restored } = useContext(AuthContext);
const [showPassword, setShowPassword] = useState(false);
useEffect(() => {
if (restored && user) {
navigation.reset({
index: 0,
routes: [{ name: "MyDrawer" }],
});
}
}, [user, restored]);
const LoginSchema = Yup.object().shape({
email: Yup.string().email("Invalid email").required("Email required"),
password: Yup.string().min(4, "Too short").required("Password required"),
});
return (
<ImageBackground
source={{
uri: "https://plus.unsplash.com/premium_photo-1672082518036-92db94a85341?q=80&w=327&auto=format&fit=crop",
}}
style={styles.background}
resizeMode="cover"
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<Formik
initialValues={{ email: "", password: "" }}
validationSchema={LoginSchema}
onSubmit={async (values, { setSubmitting }) => {
try {
await signIn(values);
navigation.reset({
index: 0,
routes: [{ name: "MyDrawer" }],
});
} catch (e) {
Alert.alert("Login failed", e.message || "Check credentials");
} finally {
setSubmitting(false);
}
}}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
touched,
isSubmitting,
}) => {
const isFormFilled = values.email && values.password;
return (
<View style={styles.formContainer}>
<Text style={styles.h}>Infinity Store</Text>
<TextInput
label="Email"
mode="outlined"
value={values.email}
onChangeText={handleChange("email")}
onBlur={handleBlur("email")}
autoCapitalize="none"
style={styles.input}
error={touched.email && !!errors.email}
left={<TextInput.Icon icon={() => <Icon name="email" size={20} />} />}
/>
{touched.email && errors.email && (
<Text style={styles.errorText}>{errors.email}</Text>
)}
<TextInput
label="Password"
mode="outlined"
value={values.password}
onChangeText={handleChange("password")}
onBlur={handleBlur("password")}
secureTextEntry={!showPassword}
style={styles.input}
error={touched.password && !!errors.password}
left={<TextInput.Icon icon={() => <Icon name="lock" size={20} />} />}
right={
<TextInput.Icon
icon={() => (
<Icon
name={showPassword ? "visibility" : "visibility-off"}
size={20}
/>
)}
onPress={() => setShowPassword(!showPassword)}
/>
}
/>
{touched.password && errors.password && (
<Text style={styles.errorText}>{errors.password}</Text>
)}
<Button
mode="contained"
onPress={handleSubmit}
loading={isSubmitting}
disabled={!isFormFilled || isSubmitting}
style={styles.loginBtn}
buttonColor="orange"
textColor="white"
>
{isSubmitting ? "Logging in..." : "Login"}
</Button>
<Button
mode="text"
onPress={() => navigation.navigate("Signup")}
textColor="gray"
>
Create Account
</Button>
</View>
);
}}
</Formik>
</KeyboardAvoidingView>
</TouchableWithoutFeedback>
</ImageBackground>
);
}
const styles = StyleSheet.create({
background: { flex: 1 },
container: { flex: 1, justifyContent: "center", padding: 20 },
formContainer: {
backgroundColor: "rgba(255,255,255,0.85)",
padding: 20,
borderRadius: 12,
},
input: { marginBottom: 10 },
h: {
fontSize: 35,
fontStyle: "italic",
fontWeight: "700",
marginBottom: 20,
textAlign: "center",
},
errorText: { color: "red", marginBottom: 8, marginLeft: 5 },
loginBtn: { marginTop: 10, marginBottom: 10, borderRadius: 8 },
});
//
//
//src/screens/SignupScreen.js
// SignupScreen.js (updated)
import React, { useContext, useState } from "react";
import {
View,
StyleSheet,
ImageBackground,
KeyboardAvoidingView,
Platform,
Modal,
Animated,
Keyboard,
TouchableWithoutFeedback,
Alert,
} from "react-native";
import { TextInput, Button, Text } from "react-native-paper";
import { Formik } from "formik";
import * as Yup from "yup";
import { AuthContext } from "../AuthContext";
import Icon from "react-native-vector-icons/MaterialIcons";
export default function SignupScreen({ navigation }) {
const { signUp } = useContext(AuthContext);
const [showPassword, setShowPassword] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [showModal, setShowModal] = useState(false);
const [serverError, setServerError] = useState(null);
const fadeAnim = useState(new Animated.Value(0))[0]; // for animation
const SignupSchema = Yup.object().shape({
name: Yup.string().required("Name required"),
email: Yup.string().email("Invalid email").required("Email required"),
password: Yup.string().min(4, "Too short").required("Password required"),
confirmPassword: Yup.string()
.oneOf([Yup.ref("password")], "Passwords must match")
.required("Confirm your password"),
});
const showSuccessModal = () => {
setShowModal(true);
Animated.timing(fadeAnim, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}).start();
setTimeout(() => {
Animated.timing(fadeAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start(() => {
setShowModal(false);
navigation.navigate("Login");
});
}, 2000); // slightly shorter delay
};
return (
<ImageBackground
source={{
uri:
"https://plus.unsplash.com/premium_photo-1672082518036-92db94a85341?q=80&w=327&auto=format&fit=crop",
}}
style={styles.background}
resizeMode="cover"
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={{ flex: 1 }}>
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<Formik
initialValues={{
name: "",
email: "",
password: "",
confirmPassword: "",
}}
validationSchema={SignupSchema}
onSubmit={async (values, { setSubmitting, resetForm }) => {
setServerError(null);
try {
await signUp({
name: values.name,
email: values.email,
password: values.password,
});
resetForm();
showSuccessModal(); // show animated popup
} catch (e) {
// show error properly
const msg = e?.message || "Something went wrong";
setServerError(msg);
Alert.alert("Signup failed", msg);
} finally {
setSubmitting(false);
}
}}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
touched,
isSubmitting,
}) => {
const isFormFilled =
values.name && values.email && values.password && values.confirmPassword;
return (
<View style={styles.formContainer}>
<Text style={styles.h}>Infinity Store</Text>
<TextInput
label="Name"
mode="outlined"
value={values.name}
onChangeText={handleChange("name")}
onBlur={handleBlur("name")}
style={styles.input}
error={touched.name && !!errors.name}
left={<TextInput.Icon icon={() => <Icon name="person" size={20} />} />}
/>
{touched.name && errors.name && (
<Text style={styles.errorText}>{errors.name}</Text>
)}
<TextInput
label="Email"
mode="outlined"
value={values.email}
onChangeText={handleChange("email")}
onBlur={handleBlur("email")}
autoCapitalize="none"
style={styles.input}
error={touched.email && !!errors.email}
left={<TextInput.Icon icon={() => <Icon name="email" size={20} />} />}
/>
{touched.email && errors.email && (
<Text style={styles.errorText}>{errors.email}</Text>
)}
{/* Password Field with Eye Toggle */}
<TextInput
label="Password"
mode="outlined"
value={values.password}
onChangeText={handleChange("password")}
onBlur={handleBlur("password")}
secureTextEntry={!showPassword}
style={styles.input}
error={touched.password && !!errors.password}
left={<TextInput.Icon icon={() => <Icon name="lock" size={20} />} />}
right={
<TextInput.Icon
icon={() => (
<Icon name={showPassword ? "visibility" : "visibility-off"} size={20} />
)}
onPress={() => setShowPassword(!showPassword)}
/>
}
/>
{touched.password && errors.password && (
<Text style={styles.errorText}>{errors.password}</Text>
)}
{/* Confirm Password Field */}
<TextInput
label="Confirm Password"
mode="outlined"
value={values.confirmPassword}
onChangeText={handleChange("confirmPassword")}
onBlur={handleBlur("confirmPassword")}
secureTextEntry={!showConfirm}
style={styles.input}
error={touched.confirmPassword && !!errors.confirmPassword}
left={<TextInput.Icon icon={() => <Icon name="lock-outline" size={20} />} />}
right={
<TextInput.Icon
icon={() => (
<Icon name={showConfirm ? "visibility" : "visibility-off"} size={20} />
)}
onPress={() => setShowConfirm(!showConfirm)}
/>
}
/>
{touched.confirmPassword && errors.confirmPassword && (
<Text style={styles.errorText}>{errors.confirmPassword}</Text>
)}
{/* inline server error */}
{serverError ? <Text style={styles.serverError}>{serverError}</Text> : null}
<Button
mode="contained"
onPress={handleSubmit}
loading={isSubmitting}
disabled={!isFormFilled || isSubmitting}
style={styles.signupBtn}
buttonColor="orange"
textColor="white"
>
{isSubmitting ? "Signing up..." : "Sign Up"}
</Button>
<Button mode="text" onPress={() => navigation.goBack()} textColor="gray">
Back to Login
</Button>
</View>
);
}}
</Formik>
</KeyboardAvoidingView>
{/* Animated Success Modal */}
<Modal visible={showModal} transparent animationType="fade">
<View style={styles.modalOverlay}>
<Animated.View style={[styles.modalCard, { opacity: fadeAnim }]}>
<Icon name="check-circle" size={60} color="green" />
<Text style={styles.modalText}>Registered Successfully!</Text>
<Text style={styles.modalSub}>Redirecting to Login...</Text>
</Animated.View>
</View>
</Modal>
</View>
</TouchableWithoutFeedback>
</ImageBackground>
);
}
const styles = StyleSheet.create({
background: { flex: 1 },
container: { flex: 1, justifyContent: "center", padding: 20 },
formContainer: {
backgroundColor: "rgba(255,255,255,0.85)",
padding: 20,
borderRadius: 12,
},
input: { marginBottom: 10 },
h: {
fontSize: 35,
fontStyle: "italic",
fontWeight: "700",
marginBottom: 20,
textAlign: "center",
},
errorText: { color: "red", marginBottom: 8, marginLeft: 5 },
serverError: { color: "red", marginBottom: 8, marginLeft: 5, textAlign: "center" },
signupBtn: { marginTop: 10, marginBottom: 10, borderRadius: 8 },
// Modal styles
modalOverlay: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0,0,0,0.4)",
},
modalCard: {
width: "80%",
backgroundColor: "white",
borderRadius: 15,
padding: 25,
alignItems: "center",
elevation: 5,
},
modalText: {
fontSize: 20,
fontWeight: "bold",
marginTop: 10,
color: "green",
},
modalSub: {
fontSize: 14,
color: "gray",
marginTop: 5,
},
});
//npm command for dependency
npm install @react-native-async-storage/async-storage@^2.2.0 \
@react-native/new-app-screen@0.81.4 \
@react-navigation/bottom-tabs@^7.4.7 \
@react-navigation/drawer@^7.5.8 \
@react-navigation/native@^7.1.17 \
@react-navigation/stack@^7.4.8 \
express@^5.1.0 \
formik@^2.4.6 \
react@19.1.0 \
react-native@0.81.4 \
react-native-dropdown-picker@^5.4.6 \
react-native-gesture-handler@^2.28.0 \
react-native-image-picker@^8.2.1 \
react-native-paper@^5.14.5 \
react-native-reanimated@^4.1.2 \
react-native-safe-area-context@^5.6.1 \
react-native-screens@^4.16.0 \
react-native-vector-icons@^10.3.0 \
react-native-worklets@^0.6.0 \
yup@^1.7.1
//
//ipconfig
//npx json-server db.json