diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1ffa924 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# Use NGROK URL here instead of localhost +EXPO_PUBLIC_API_BASE_URL="" diff --git a/.gitignore b/.gitignore index ec67e6b..419232f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ dist/ web-build/ expo-env.d.ts +.env + # Native .kotlin/ *.orig.* diff --git a/App.js b/App.js index 0143074..3c7ab0e 100644 --- a/App.js +++ b/App.js @@ -1,3 +1,5 @@ +import { NavigationContainer } from "@react-navigation/native"; +import { QueryClientProvider } from "@tanstack/react-query"; import { ActivityIndicator, Text, View } from "react-native"; import { SafeAreaProvider } from "react-native-safe-area-context"; import FontLoaderWrapper from "./components/FontLoaderWrapper"; @@ -5,6 +7,7 @@ import SystemUIManager from "./components/SystemUIManager"; import { COLORS } from "./constants/colors"; import { AppProvider } from "./contexts/AppContext"; import { useInitDb } from "./hooks/useInitDb"; +import queryClient from "./lib/queryClient"; import StackNavigation from "./navigation/StackNavigation"; export default function App() { @@ -24,12 +27,16 @@ export default function App() { return ( - - - - - - + + + + + + + + + + ); } diff --git a/app.json b/app.json index ab7d93c..7e8e61b 100644 --- a/app.json +++ b/app.json @@ -2,8 +2,6 @@ "expo": { "name": "travel-buddy", "slug": "travel-buddy", - - // After adding a custom scheme to your app, you need to create a new development build. Once the app is installed on a device, you can open links within your app using myapp: "scheme": "travel-buddy", "version": "1.0.0", "orientation": "portrait", @@ -47,7 +45,8 @@ } } ], - "expo-image-picker" + "expo-image-picker", + "expo-font" ], "extra": { "eas": { diff --git a/babel.config.js b/babel.config.js index 1d45dab..8091722 100644 --- a/babel.config.js +++ b/babel.config.js @@ -2,9 +2,12 @@ module.exports = function (api) { api.cache(true); return { presets: ["babel-preset-expo"], + plugins: [ + "react-native-reanimated/plugin", // MUST be last + ], env: { production: { - plugins: ["transform-remove-console"], + plugins: ["transform-remove-console", "react-native-reanimated/plugin"], }, }, }; diff --git a/components/ActivityIndicator.js b/components/ActivityIndicator.js new file mode 100644 index 0000000..407107c --- /dev/null +++ b/components/ActivityIndicator.js @@ -0,0 +1,32 @@ +import { ActivityIndicator, StyleSheet } from "react-native"; +import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; +import { COLORS } from "../constants/colors"; + +/** + * size : large, small + * color : color of the spinner (optional, defaults to buttonGradientEnd) + */ +const Spinner = ({ size = "small", color }) => ( + + + + + +); + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center", + }, + horizontal: { + flexDirection: "row", + justifyContent: "space-around", + padding: 10, + }, +}); + +export default Spinner; diff --git a/components/ImagePickerComponent.js b/components/ImagePickerComponent.js index 69a9a9d..e629603 100644 --- a/components/ImagePickerComponent.js +++ b/components/ImagePickerComponent.js @@ -36,7 +36,7 @@ const ImagePickerComponent = ({ if (!hasPermission) { Alert.alert( "Permission required", - "Please grant gallery access to select images" + "Please grant gallery access to select images", ); return; } @@ -65,7 +65,7 @@ const ImagePickerComponent = ({ if (!hasPermission) { Alert.alert( "Permission required", - "Please grant camera access to take photos" + "Please grant camera access to take photos", ); return; } diff --git a/components/InputField.js b/components/InputField.js index c3f2ee3..a6273d1 100644 --- a/components/InputField.js +++ b/components/InputField.js @@ -25,6 +25,7 @@ const InputField = ({ autoCapitalize = "sentences", autoFocus = false, maxLength, + ...other }) => { const [isFocused, setIsFocused] = useState(false); @@ -67,11 +68,11 @@ const InputField = ({ secureTextEntry={secureTextEntry} keyboardType={keyboardType} autoCapitalize={autoCapitalize} - editable={!disabled} autoFocus={autoFocus} onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} maxLength={maxLength} + {...other} /> {rightIcon && ( { + return ( + + + + +

{title}

+
+ ); +}; + +const styles = StyleSheet.create({ + button: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + height: 48, + borderRadius: 22, + borderWidth: 1.5, + borderColor: COLORS.buttonGradientEnd, + marginBottom: 32, // Increased margin to ensure visibility above FAB + }, + icon: { + marginRight: 8, + }, + text: { + fontSize: 16, + fontWeight: "600", + color: COLORS.buttonGradientEnd, + }, +}); + +export default OutlineButton; diff --git a/components/ProfileCard.js b/components/ProfileCard.js new file mode 100644 index 0000000..f38b723 --- /dev/null +++ b/components/ProfileCard.js @@ -0,0 +1,87 @@ +import { StyleSheet, View } from "react-native"; +import { COLORS } from "../constants"; +import RoundedAvatar from "./RoundedAvatar"; +import { H1, P } from "./Typography"; + +const ProfileCard = ({ name, phone, location, avatar, onEditAvatar }) => { + return ( + + + {name &&

{name}

} + {phone &&

{phone}

} +

{location}

+
+ ); +}; + +const styles = StyleSheet.create({ + profileCard: { + backgroundColor: COLORS.surface, + borderRadius: 16, + padding: 24, + alignItems: "center", + shadowColor: COLORS.shadowColor, + shadowOffset: { + width: 0, + height: 4, + }, + shadowOpacity: 0.08, + shadowRadius: 12, + elevation: 4, + marginBottom: 24, + }, + avatarContainer: { + position: "relative", + marginBottom: 16, + }, + avatarPlaceholder: { + width: 96, + height: 96, + borderRadius: 48, + backgroundColor: COLORS.inputBackground, + alignItems: "center", + justifyContent: "center", + borderWidth: 1, + borderColor: COLORS.buttonGradientEnd, + borderStyle: "solid", + }, + avatarImage: { + width: 96, + height: 96, + borderRadius: 48, + borderWidth: 1, + borderColor: COLORS.buttonGradientEnd, + borderStyle: "solid", + }, + editButton: { + position: "absolute", + bottom: 0, + right: 0, + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: COLORS.buttonGradientEnd, + alignItems: "center", + justifyContent: "center", + }, + profileName: { + fontSize: 22, + fontFamily: "PlayfairDisplayBold", + color: COLORS.textPrimary, + marginBottom: 4, + textAlign: "center", + }, + profilePhone: { + fontSize: 14, + color: COLORS.textSecondary, + marginBottom: 4, + textAlign: "center", + }, + profileLocation: { + fontSize: 13, + color: COLORS.textTertiary, + textAlign: "center", + }, +}); + +export default ProfileCard; diff --git a/components/RoundedAvatar.js b/components/RoundedAvatar.js new file mode 100644 index 0000000..8667fb6 --- /dev/null +++ b/components/RoundedAvatar.js @@ -0,0 +1,69 @@ +import { Image, StyleSheet, View } from "react-native"; +import Icon from "react-native-vector-icons/Ionicons"; +import { COLORS } from "../constants"; + +const RoundedAvatar = ({ onEditAvatar, avatar, showEditButton }) => { + return ( + + {avatar ? ( + profileImage + ) : ( + + + + + + )} + {showEditButton && ( + + + + + + )} + + ); +}; + +const styles = StyleSheet.create({ + avatarContainer: { + position: "relative", + marginBottom: 16, + }, + avatarPlaceholder: { + width: 96, + height: 96, + borderRadius: 48, + backgroundColor: COLORS.inputBackground, + alignItems: "center", + justifyContent: "center", + borderWidth: 1, + borderColor: COLORS.buttonGradientEnd, + borderStyle: "solid", + }, + avatarImage: { + width: 96, + height: 96, + borderRadius: 48, + borderWidth: 1, + borderColor: COLORS.buttonGradientEnd, + borderStyle: "solid", + }, + editButton: { + position: "absolute", + bottom: 0, + right: 0, + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: COLORS.buttonGradientEnd, + alignItems: "center", + justifyContent: "center", + }, +}); + +export default RoundedAvatar; diff --git a/components/SettingsList.js b/components/SettingsList.js new file mode 100644 index 0000000..c63322d --- /dev/null +++ b/components/SettingsList.js @@ -0,0 +1,24 @@ +import { FlatList, View } from "react-native"; +import SettingsListItem from "./SettingsListItem"; + +const SettingsList = ({ items, dividerStyle }) => { + return ( + item.label} + renderItem={({ item, index }) => ( + + + {index !== items.length - 1 && } + + )} + scrollEnabled={false} + /> + ); +}; + +export default SettingsList; diff --git a/components/SettingsListItem.js b/components/SettingsListItem.js new file mode 100644 index 0000000..2e256b3 --- /dev/null +++ b/components/SettingsListItem.js @@ -0,0 +1,51 @@ +import { StyleSheet, TouchableOpacity, View } from "react-native"; +import Icon from "react-native-vector-icons/Ionicons"; +import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; +import { COLORS } from "../constants"; +import { P } from "./Typography"; + +const SettingsListItem = ({ icon, label, onPress }) => { + return ( + + + + + + +

{label}

+ + + +
+ ); +}; + +const styles = StyleSheet.create({ + settingsItem: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 16, + paddingHorizontal: 16, + }, + iconContainer: { + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: COLORS.inputBackground, + alignItems: "center", + justifyContent: "center", + marginRight: 16, + }, + settingsLabel: { + flex: 1, + fontSize: 16, + color: COLORS.textPrimary, + fontWeight: "500", + }, +}); + +export default SettingsListItem; diff --git a/components/Swiper.js b/components/Swiper.js index a2e5d97..c068376 100644 --- a/components/Swiper.js +++ b/components/Swiper.js @@ -13,7 +13,9 @@ import { } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { COLORS } from "../constants/colors"; -import { Splash1, Splash2, Splash3 } from "../screens"; +import Splash1 from "../screens/Splash1"; +import Splash2 from "../screens/Splash2"; +import Splash3 from "../screens/Splash3"; import SplashStyles from "./styles/SplashStyles"; import { useAppContext } from "../contexts/AppContext"; @@ -83,7 +85,7 @@ const Swiper = () => { } }, }), - [currentIndex, completeSplash] + [currentIndex, completeSplash], ); // Add dependencies to prevent unnecessary re-creation // Memoize the screens to avoid re-creation on each render diff --git a/components/TextArea.js b/components/TextArea.js index d424581..ca46508 100644 --- a/components/TextArea.js +++ b/components/TextArea.js @@ -1,5 +1,4 @@ -import React from "react"; -import { View, Text, TextInput, StyleSheet } from "react-native"; +import { StyleSheet, Text, TextInput, View } from "react-native"; import Icon from "react-native-vector-icons/MaterialIcons"; import { COLORS } from "../constants/colors"; diff --git a/components/Typography.js b/components/Typography.js index e57dc76..567804d 100644 --- a/components/Typography.js +++ b/components/Typography.js @@ -1,4 +1,3 @@ -import React from "react"; import { Text, StyleSheet } from "react-native"; import { COLORS } from "../constants/colors"; diff --git a/components/index.js b/components/index.js index 3a1378c..e2ddabf 100644 --- a/components/index.js +++ b/components/index.js @@ -1,4 +1,19 @@ +export { default as Spinner } from "./ActivityIndicator"; +export { default as Background } from "./Background"; export { default as BottomNavigation } from "./BottomNavigation"; +export { default as CommonHeader } from "./CommonHeader"; export { default as FontLoaderWrapper } from "./FontLoaderWrapper"; +export { default as ImageBackgroundWrapper } from "./ImageBackgroundWrapper"; +export { default as InputField } from "./InputField"; +export { default as OutlineButton } from "./OutlineButton"; +export { default as PrimaryButton } from "./PrimaryButton"; +export { default as ProfileCard } from "./ProfileCard"; +export { default as RoundedAvatar } from "./RoundedAvatar"; +export { default as SecondaryButton } from "./SecondaryButton"; +export { default as SettingsList } from "./SettingsList"; +export { default as SettingsListItem } from "./SettingsListItem"; export { default as SplashStyles } from "./styles/SplashStyles"; export { default as Swiper } from "./Swiper"; +export { default as SystemUIManager } from "./SystemUIManager"; +export { default as TextArea } from "./TextArea"; +export { Caption, H1, H2, H3, LinkText, P } from "./Typography"; diff --git a/contexts/AppContext.js b/contexts/AppContext.js index fcb8595..88ca41b 100644 --- a/contexts/AppContext.js +++ b/contexts/AppContext.js @@ -1,27 +1,29 @@ -import React, { createContext, useContext, useState, useEffect } from "react"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import { createContext, useContext, useEffect, useState } from "react"; +import { useAuthStore } from "../store"; // Create the context const AppContext = createContext(); // Create a provider component export const AppProvider = ({ children }) => { - const [appState, setAppState] = useState(null); // null = loading, 'splash', 'auth', 'main' + const [appState, setAppState] = useState("splash"); // null = loading, 'splash', 'auth', 'main' + const { isAuthenticated } = useAuthStore(); useEffect(() => { const checkAppStatus = async () => { try { // Check if splash screen should be shown const shouldShowSplash = await AsyncStorage.getItem( - "should_show_splash_screen" + "should_show_splash_screen", ); // If should_show_splash_screen is not set or is "true", show splash - // Otherwise, go directly to auth + // Otherwise, check authentication status if (shouldShowSplash !== "false") { setAppState("splash"); } else { - setAppState("auth"); + setAppState(isAuthenticated ? "main" : "auth"); } } catch (error) { console.error("Error reading app status from AsyncStorage:", error); @@ -31,42 +33,34 @@ export const AppProvider = ({ children }) => { }; checkAppStatus(); - }, []); - - const navigateToAuth = async () => { - try { - // Mark that the user has seen the splash screen - await AsyncStorage.setItem("should_show_splash_screen", "false"); - // This function is called after authentication is complete - await AsyncStorage.setItem("isAuthenticated", "true"); - setAppState("main"); // Show main app - } catch (error) { - console.error("Error saving app status to AsyncStorage:", error); - } - }; - - const completeSplash = async () => { - try { - // Mark that the user has seen the splash screen but not authenticated yet - await AsyncStorage.setItem("should_show_splash_screen", "false"); - setAppState("auth"); // Show auth screens - } catch (error) { - console.error("Error saving splash status to AsyncStorage:", error); - } - }; + }, [isAuthenticated]); - const value = { - appState, - navigateToAuth, - completeSplash, - }; + console.debug("AppProvider appState:", appState); // Don't render anything until the app state is loaded - if (appState === null) { - return null; // Or return a loading component if you prefer + if (!appState) { + return children; // allow navigation to mount } - return {children}; + return ( + { + console.log("navigateToAuth called"); + await AsyncStorage.setItem("should_show_splash_screen", "false"); + setAppState("auth"); + }, + navigateToMain: () => setAppState("main"), + completeSplash: async () => { + await AsyncStorage.setItem("should_show_splash_screen", "false"); + setAppState("auth"); + }, + }} + > + {children} + + ); }; // Custom hook to use the context diff --git a/contexts/AuthContext.js b/contexts/AuthContext.js new file mode 100644 index 0000000..49ed36c --- /dev/null +++ b/contexts/AuthContext.js @@ -0,0 +1,42 @@ +import { createContext, useContext, useEffect, useState } from "react"; +import { useAuthStore } from "../store"; + +const AuthContext = createContext(); + +// AuthProvider should be a proper React component that accepts children +export const AuthProvider = ({ children }) => { + const [initialized, setInitialized] = useState(false); + + const { user, isAuthenticated, fetchProfile } = useAuthStore(); + + useEffect(() => { + const initializeAuth = async () => { + // Only fetch profile if user is not authenticated and no user exists + if (!isAuthenticated && !user) { + await fetchProfile(); + } + setInitialized(true); + }; + + initializeAuth(); + }, [isAuthenticated, user]); + + // Show loading state while initializing auth + if (!initialized) { + return null; // Or a loading spinner component + } + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; diff --git a/hooks/useLogin.js b/hooks/useLogin.js new file mode 100644 index 0000000..c44c2c7 --- /dev/null +++ b/hooks/useLogin.js @@ -0,0 +1,12 @@ +import { useMutation } from "@tanstack/react-query"; +import { loginApi } from "../services/auth.api"; +import handleApiError from "../utils/handleApiError"; + +const useLogin = () => { + return useMutation({ + mutationFn: loginApi, + onError: handleApiError, + }); +}; + +export default useLogin; diff --git a/hooks/useProfile.js b/hooks/useProfile.js new file mode 100644 index 0000000..1925acb --- /dev/null +++ b/hooks/useProfile.js @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import { fetchProfileApi } from "../services/auth.api"; + +const useProfile = () => { + return useQuery({ + queryKey: ["profile"], + queryFn: fetchProfileApi, + retryDelay: 1000, // 1s delay + retry: 2, + refetchOnReconnect: true, + refetchOnWindowFocus: true, + staleTime: 0, + }); +}; + +export default useProfile; diff --git a/hooks/useRegister.js b/hooks/useRegister.js new file mode 100644 index 0000000..439ae36 --- /dev/null +++ b/hooks/useRegister.js @@ -0,0 +1,10 @@ +import { useMutation } from "@tanstack/react-query"; +import { registerApi } from "../services/auth.api"; + +const useRegister = () => { + return useMutation({ + mutationFn: registerApi, + }); +}; + +export default useRegister; diff --git a/hooks/useUpdateProfile.js b/hooks/useUpdateProfile.js new file mode 100644 index 0000000..0e5fe22 --- /dev/null +++ b/hooks/useUpdateProfile.js @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { updateProfileApi } from "../services/auth.api"; +import { useAuthStore } from "../store"; +import handleApiError from "../utils/handleApiError"; + +const useUpdateProfile = () => { + const queryClient = useQueryClient(); + const { setAuth } = useAuthStore(); + + return useMutation({ + mutationFn: updateProfileApi, + onSuccess: (data) => { + // The API might return just the updated user or an object with user/token + // Handle both cases + const userData = data.user || data; + const token = data.token; + + // Update the auth store with the new user data if user object exists + if (userData) { + setAuth({ user: userData, token: token || undefined }); + } + + // Invalidate and refetch profile query to update UI + queryClient.invalidateQueries({ queryKey: ["profile"] }); + }, + onError: handleApiError, + }); +}; + +export default useUpdateProfile; diff --git a/lib/queryClient.js b/lib/queryClient.js new file mode 100644 index 0000000..e6fecaa --- /dev/null +++ b/lib/queryClient.js @@ -0,0 +1,11 @@ +import { QueryClient } from "@tanstack/react-query"; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 2, // retry failed requests twice + staleTime: 1000 * 60, // 1 minute cache freshness + }, + }, +}); + +export default queryClient; diff --git a/navigation/BottomTabNavigator.js b/navigation/BottomTabNavigator.js index 8d36888..56cd58c 100644 --- a/navigation/BottomTabNavigator.js +++ b/navigation/BottomTabNavigator.js @@ -10,13 +10,13 @@ import { MapScreen, MyTripsScreen, TravelByScreen, + UserProfileScreen, } from "../screens"; // Import the bottom navigation component import BottomNavigation from "../components/BottomNavigation"; const BlogsScreen = lazy(() => import("../screens/BlogsScreen")); -const UserProfileScreen = lazy(() => import("../screens/UserProfileScreen")); const Loader = () => ( @@ -26,6 +26,19 @@ const Loader = () => ( const Tab = createBottomTabNavigator(); +// Custom tab bar component that renders our BottomNavigation +const BottomNavigationTabBar = (props) => { + return ( + + + + ); +}; + const BottomTabNavigator = () => { return ( }> @@ -47,19 +60,6 @@ const BottomTabNavigator = () => { ); }; -// Custom tab bar component that renders our BottomNavigation -const BottomNavigationTabBar = (props) => { - return ( - - - - ); -}; - const styles = StyleSheet.create({ tabBarContainer: { position: "absolute", diff --git a/navigation/StackNavigation.js b/navigation/StackNavigation.js index 7077523..38af4a2 100644 --- a/navigation/StackNavigation.js +++ b/navigation/StackNavigation.js @@ -1,5 +1,5 @@ -import { NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import { Spinner } from "../components"; import Swiper from "../components/Swiper"; import { useAppContext } from "../contexts/AppContext"; import ContributorsViewAllScreen from "../screens/ContributorsViewAllScreen"; @@ -25,80 +25,75 @@ const Stack = createNativeStackNavigator(); const StackNavigation = () => { const { appState } = useAppContext(); + if (!appState) { + return ; // or loader + } + switch (appState) { case "splash": // Show splash screens return ( - - - - - + + + ); case "auth": // Show authentication flow (Login/Register/OTP) return ( - - - - - - - + + + + + ); case "main": // Show main app with bottom tabs return ( - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ); default: // Loading state - return nothing or a loading component diff --git a/package-lock.json b/package-lock.json index 678e5b5..333ece9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,12 +19,14 @@ "@react-navigation/native": "^7.1.25", "@react-navigation/native-stack": "^7.8.6", "@react-navigation/stack": "^7.6.13", - "babel-preset-expo": "~54.0.9", - "expo": "~54.0.31", + "@tanstack/react-query": "^5.90.20", + "axios": "^1.13.4", + "babel-preset-expo": "~54.0.10", + "expo": "~54.0.33", "expo-build-properties": "~1.0.10", "expo-constants": "~18.0.13", "expo-dev-client": "~6.0.20", - "expo-font": "^14.0.10", + "expo-font": "~14.0.11", "expo-image": "~3.0.11", "expo-image-picker": "~17.0.10", "expo-linear-gradient": "^15.0.8", @@ -42,16 +44,19 @@ "react-native-gesture-handler": "~2.28.0", "react-native-pager-view": "6.9.1", "react-native-progress": "^5.0.1", + "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "15.12.1", "react-native-vector-icons": "^10.3.0", + "react-native-worklets": "0.5.1", "yup": "^1.7.1", - "zustand": "^5.0.9" + "zustand": "^5.0.11" }, "devDependencies": { "@eslint/js": "^9.39.2", "@react-native/babel-preset": "^0.83.1", + "@types/axios": "^0.9.36", "@types/react": "~19.1.10", "@typescript-eslint/eslint-plugin": "^8.50.1", "@typescript-eslint/parser": "^8.50.1", @@ -1393,7 +1398,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -2364,12 +2368,12 @@ } }, "node_modules/@expo/metro/node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -2742,9 +2746,9 @@ } }, "node_modules/@expo/package-manager": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.9.tgz", - "integrity": "sha512-Nv5THOwXzPprMJwbnXU01iXSrCp3vJqly9M4EJ2GkKko9Ifer2ucpg7x6OUsE09/lw+npaoUnHMXwkw7gcKxlg==", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.10.tgz", + "integrity": "sha512-axJm+NOj3jVxep49va/+L3KkF3YW/dkV+RwzqUJedZrv4LeTqOG4rhrCaCPXHTvLqCTDKu6j0Xyd28N7mnxsGA==", "license": "MIT", "dependencies": { "@expo/json-file": "^10.0.8", @@ -2884,20 +2888,33 @@ "license": "MIT" }, "node_modules/@expo/xcpretty": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.3.2.tgz", - "integrity": "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.0.tgz", + "integrity": "sha512-o2qDlTqJ606h4xR36H2zWTywmZ3v3842K6TU8Ik2n1mfW0S580VHlt3eItVYdLYz+klaPp7CXqanja8eASZjRw==", "license": "BSD-3-Clause", "dependencies": { - "@babel/code-frame": "7.10.4", + "@babel/code-frame": "^7.20.0", "chalk": "^4.1.0", - "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, + "node_modules/@expo/xcpretty/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@expo/xcpretty/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2953,22 +2970,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/@expo/xcpretty/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@expo/xcpretty/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2990,36 +2991,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@expo/xcpretty/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@expo/xcpretty/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@expo/xcpretty/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4184,6 +4155,39 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", + "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/axios": { + "version": "0.9.36", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz", + "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4937,6 +4941,12 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -4958,6 +4968,17 @@ "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", "license": "MIT" }, + "node_modules/axios": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -5210,9 +5231,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "54.0.9", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.9.tgz", - "integrity": "sha512-8J6hRdgEC2eJobjoft6mKJ294cLxmi3khCUy2JJQp4htOYYkllSLUq6vudWJkTJiIuGdVR4bR6xuz2EvJLWHNg==", + "version": "54.0.10", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.10.tgz", + "integrity": "sha512-wTt7POavLFypLcPW/uC5v8y+mtQKDJiyGLzYCjqr9tx0Qc3vCXcDKk1iCFIj/++Iy5CWhhTflEa7VvVPNWeCfw==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -5886,6 +5907,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -6276,6 +6309,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6615,7 +6657,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7141,26 +7182,26 @@ "license": "MIT" }, "node_modules/expo": { - "version": "54.0.31", - "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.31.tgz", - "integrity": "sha512-kQ3RDqA/a59I7y+oqQGyrPbbYlgPMUdKBOgvFLpoHbD2bCM+F75i4N0mUijy7dG5F/CUCu2qHmGGUCXBbMDkCg==", + "version": "54.0.33", + "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz", + "integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "54.0.21", + "@expo/cli": "54.0.23", "@expo/config": "~12.0.13", "@expo/config-plugins": "~54.0.4", "@expo/devtools": "0.1.8", "@expo/fingerprint": "0.15.4", "@expo/metro": "~54.2.0", - "@expo/metro-config": "54.0.13", + "@expo/metro-config": "54.0.14", "@expo/vector-icons": "^15.0.3", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~54.0.9", + "babel-preset-expo": "~54.0.10", "expo-asset": "~12.0.12", "expo-constants": "~18.0.13", "expo-file-system": "~19.0.21", - "expo-font": "~14.0.10", + "expo-font": "~14.0.11", "expo-keep-awake": "~15.0.8", "expo-modules-autolinking": "3.0.24", "expo-modules-core": "3.0.29", @@ -7334,9 +7375,9 @@ } }, "node_modules/expo-font": { - "version": "14.0.10", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.10.tgz", - "integrity": "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q==", + "version": "14.0.11", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz", + "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==", "license": "MIT", "dependencies": { "fontfaceobserver": "^2.1.0" @@ -7628,12 +7669,12 @@ } }, "node_modules/expo/node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -7642,9 +7683,9 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "54.0.21", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.21.tgz", - "integrity": "sha512-L/FdpyZDsg/Nq6xW6kfiyF9DUzKfLZCKFXEVZcDqCNar6bXxQVotQyvgexRvtUF5nLinuT/UafLOdC3FUALUmA==", + "version": "54.0.23", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.23.tgz", + "integrity": "sha512-km0h72SFfQCmVycH/JtPFTVy69w6Lx1cHNDmfLfQqgKFYeeHTjx7LVDP4POHCtNxFP2UeRazrygJhlh4zz498g==", "license": "MIT", "dependencies": { "@0no-co/graphql.web": "^1.0.8", @@ -7656,9 +7697,9 @@ "@expo/image-utils": "^0.8.8", "@expo/json-file": "^10.0.8", "@expo/metro": "~54.2.0", - "@expo/metro-config": "~54.0.13", + "@expo/metro-config": "~54.0.14", "@expo/osascript": "^2.3.8", - "@expo/package-manager": "^1.9.9", + "@expo/package-manager": "^1.9.10", "@expo/plist": "^0.4.8", "@expo/prebuild-config": "^54.0.8", "@expo/schema-utils": "^0.1.8", @@ -7750,9 +7791,9 @@ } }, "node_modules/expo/node_modules/@expo/metro-config": { - "version": "54.0.13", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.13.tgz", - "integrity": "sha512-RRufMCgLR2Za1WGsh02OatIJo5qZFt31yCnIOSfoubNc3Qqe92Z41pVsbrFnmw5CIaisv1NgdBy05DHe7pEyuw==", + "version": "54.0.14", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.14.tgz", + "integrity": "sha512-hxpLyDfOR4L23tJ9W1IbJJsG7k4lv2sotohBm/kTYyiG+pe1SYCAWsRmgk+H42o/wWf/HQjE5k45S5TomGLxNA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -8141,6 +8182,26 @@ "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fontfaceobserver": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", @@ -8162,6 +8223,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formik": { "version": "2.4.9", "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.9.tgz", @@ -9914,9 +9991,9 @@ "license": "MIT" }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -9929,23 +10006,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", "cpu": [ "arm64" ], @@ -9963,9 +10040,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", "cpu": [ "arm64" ], @@ -9983,9 +10060,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", "cpu": [ "x64" ], @@ -10003,9 +10080,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", "cpu": [ "x64" ], @@ -10023,9 +10100,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", "cpu": [ "arm" ], @@ -10043,9 +10120,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", "cpu": [ "arm64" ], @@ -10063,9 +10140,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", "cpu": [ "arm64" ], @@ -10083,9 +10160,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", "cpu": [ "x64" ], @@ -10103,9 +10180,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", "cpu": [ "x64" ], @@ -10123,9 +10200,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", "cpu": [ "arm64" ], @@ -10143,9 +10220,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", "cpu": [ "x64" ], @@ -10200,9 +10277,9 @@ "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.22", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz", - "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, "node_modules/lodash.debounce": { @@ -11699,6 +11776,12 @@ "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -12211,7 +12294,6 @@ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz", "integrity": "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==", "license": "MIT", - "peer": true, "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "7.7.2" @@ -12228,7 +12310,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", - "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -12332,65 +12413,39 @@ } }, "node_modules/react-native-worklets": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.7.1.tgz", - "integrity": "sha512-KNsvR48ULg73QhTlmwPbdJLPsWcyBotrGPsrDRDswb5FYpQaJEThUKc2ncXE4UM5dn/ewLoQHjSjLaKUVPxPhA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/plugin-transform-arrow-functions": "7.27.1", - "@babel/plugin-transform-class-properties": "7.27.1", - "@babel/plugin-transform-classes": "7.28.4", - "@babel/plugin-transform-nullish-coalescing-operator": "7.27.1", - "@babel/plugin-transform-optional-chaining": "7.27.1", - "@babel/plugin-transform-shorthand-properties": "7.27.1", - "@babel/plugin-transform-template-literals": "7.27.1", - "@babel/plugin-transform-unicode-regex": "7.27.1", - "@babel/preset-typescript": "7.27.1", - "convert-source-map": "2.0.0", - "semver": "7.7.3" + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz", + "integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-arrow-functions": "^7.0.0-0", + "@babel/plugin-transform-class-properties": "^7.0.0-0", + "@babel/plugin-transform-classes": "^7.0.0-0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", + "@babel/plugin-transform-optional-chaining": "^7.0.0-0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", + "@babel/plugin-transform-template-literals": "^7.0.0-0", + "@babel/plugin-transform-unicode-regex": "^7.0.0-0", + "@babel/preset-typescript": "^7.16.7", + "convert-source-map": "^2.0.0", + "semver": "7.7.2" }, "peerDependencies": { - "@babel/core": "*", + "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, - "node_modules/react-native-worklets/node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/react-native-worklets/node_modules/@babel/preset-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", - "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.27.1" + "node_modules/react-native-worklets/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=10" } }, "node_modules/react-native/node_modules/@react-native/virtualized-lists": { @@ -13636,9 +13691,9 @@ } }, "node_modules/tar": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz", - "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -14658,9 +14713,9 @@ } }, "node_modules/zustand": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", - "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", "license": "MIT", "engines": { "node": ">=12.20.0" diff --git a/package.json b/package.json index 169c529..b3b5fe6 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,14 @@ "@react-navigation/native": "^7.1.25", "@react-navigation/native-stack": "^7.8.6", "@react-navigation/stack": "^7.6.13", - "babel-preset-expo": "~54.0.9", - "expo": "~54.0.31", + "@tanstack/react-query": "^5.90.20", + "axios": "^1.13.4", + "babel-preset-expo": "~54.0.10", + "expo": "~54.0.33", "expo-build-properties": "~1.0.10", "expo-constants": "~18.0.13", "expo-dev-client": "~6.0.20", - "expo-font": "^14.0.10", + "expo-font": "~14.0.11", "expo-image": "~3.0.11", "expo-image-picker": "~17.0.10", "expo-linear-gradient": "^15.0.8", @@ -45,17 +47,20 @@ "react-native-gesture-handler": "~2.28.0", "react-native-pager-view": "6.9.1", "react-native-progress": "^5.0.1", + "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "15.12.1", "react-native-vector-icons": "^10.3.0", + "react-native-worklets": "0.5.1", "yup": "^1.7.1", - "zustand": "^5.0.9" + "zustand": "^5.0.11" }, "private": true, "devDependencies": { "@eslint/js": "^9.39.2", "@react-native/babel-preset": "^0.83.1", + "@types/axios": "^0.9.36", "@types/react": "~19.1.10", "@typescript-eslint/eslint-plugin": "^8.50.1", "@typescript-eslint/parser": "^8.50.1", @@ -68,6 +73,7 @@ }, "overrides": { "lodash": "^4.17.22", - "tar": "^7.5.4" + "tar": "^7.5.7", + "lodash-es": "4.17.23" } } diff --git a/screens/EditProfileScreen.js b/screens/EditProfileScreen.js index 02bfc98..011ea5a 100644 --- a/screens/EditProfileScreen.js +++ b/screens/EditProfileScreen.js @@ -4,35 +4,53 @@ import { ScrollView, StyleSheet, TouchableOpacity, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import Icon from "react-native-vector-icons/Ionicons"; import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; -import Background from "../components/Background"; -import CommonHeader from "../components/CommonHeader"; -import InputField from "../components/InputField"; -import SystemUIManager from "../components/SystemUIManager"; -import { H1, P } from "../components/Typography"; +import { + Background, + CommonHeader, + H1, + InputField, + P, + RoundedAvatar, + Spinner, + SystemUIManager, +} from "../components"; import { COLORS } from "../constants/colors"; +import useProfile from "../hooks/useProfile"; +import useUpdateProfile from "../hooks/useUpdateProfile"; import { editProfileValidationSchema } from "../validation/editProfileValidation"; const EditProfileScreen = () => { const navigation = useNavigation(); + const { data, isLoading, error } = useProfile(); + const { mutate: updateProfile, isPending } = useUpdateProfile(); - const handleBack = () => { - navigation.goBack(); - }; + const user = data?.user || data; const handleSaveChanges = async (values, { setSubmitting }) => { setSubmitting(true); - // Save the updated user profile information - // Navigate back to profile screen or show success message - // In a real app, you would update the user data with an API call here - setTimeout(() => { - setSubmitting(false); - navigation.goBack(); - }, 1000); + + // Prepare profile data for update (excluding email as it's read-only) + const profileData = { + name: values.name, + phoneNumber: values.phone, + bio: values.bio, + avatar: values.avatar, + }; + + // Call the update profile API + updateProfile(profileData, { + onSuccess: () => { + setSubmitting(false); + navigation.goBack(); + }, + onError: () => { + setSubmitting(false); + }, + }); }; const handleEditAvatar = () => { // Handle avatar editing logic - console.log("Edit avatar pressed"); }; const handleChangePassword = () => { @@ -40,6 +58,34 @@ const EditProfileScreen = () => { console.log("Change password pressed"); }; + if (isLoading) { + return ( + + + + + +

Loading profile...

+
+
+
+ ); + } + + if (error) { + return ( + + + + + +

Error loading profile: {error.message}

+
+
+
+ ); + } + return ( @@ -49,10 +95,13 @@ const EditProfileScreen = () => { { handleChange, handleBlur, handleSubmit, - // eslint-disable-next-line no-unused-vars - setFieldValue, values, errors, touched, isValid, - // eslint-disable-next-line no-unused-vars - isSubmitting, }) => ( <> - {/* Profile Photo Section */} - - - - - - - - +

Change Photo

@@ -117,7 +150,7 @@ const EditProfileScreen = () => { } @@ -169,9 +202,13 @@ const EditProfileScreen = () => { -

Save Changes

+ {isPending ? ( + + ) : ( +

Save Changes

+ )}
{/* Change Password Link */} @@ -353,6 +390,18 @@ const styles = StyleSheet.create({ marginTop: 4, marginBottom: 4, }, + loadingContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + errorContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, }); export default EditProfileScreen; diff --git a/screens/HomeScreen.js b/screens/HomeScreen.js index d3fd569..dd2d3a0 100644 --- a/screens/HomeScreen.js +++ b/screens/HomeScreen.js @@ -1,14 +1,54 @@ -import { StyleSheet, Text, View } from "react-native"; -import Background from "../components/Background"; -import { P } from "../components/Typography"; +import { ActivityIndicator, StyleSheet, Text, View } from "react-native"; +import { Background, P } from "../components"; import { COLORS } from "../constants/colors"; +import useProfile from "../hooks/useProfile"; const HomeScreen = () => { + const { data, isLoading, error } = useProfile(); + + if (isLoading) { + return ( + + + +

Loading profile...

+
+
+ ); + } + + if (error) { + return ( + + + Failed to load profile + + + ); + } + + const user = data?.user || data; + return ( Home Screen -

Welcome to the home screen!

+

Welcome, {user?.name || "User"} 👋

+ + + + Name: + {user?.name || "N/A"} + + + Email: + {user?.email || "N/A"} + + + Verified: + {user?.isVerified ? "Yes" : "No"} + +
); @@ -17,6 +57,12 @@ const HomeScreen = () => { const styles = StyleSheet.create({ container: { flex: 1, + backgroundColor: COLORS.background, + }, + center: { + flex: 1, + justifyContent: "center", + alignItems: "center", }, content: { flex: 1, @@ -28,7 +74,7 @@ const styles = StyleSheet.create({ fontSize: 28, fontFamily: "PlayfairDisplayBold", color: COLORS.textPrimary, - marginBottom: 16, + marginBottom: 8, textAlign: "center", }, subtitle: { @@ -37,11 +83,51 @@ const styles = StyleSheet.create({ textAlign: "center", marginBottom: 32, }, + profileCard: { + width: "100%", + maxWidth: 400, + padding: 20, + borderRadius: 12, + backgroundColor: COLORS.surface, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 4, + }, + profileInfoRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: 8, + borderBottomWidth: 1, + borderBottomColor: COLORS.inputBorder, + }, + label: { + fontSize: 16, + fontFamily: "Inter", + fontWeight: "600", + color: COLORS.textPrimary, + }, + value: { + fontSize: 16, + fontFamily: "Inter", + color: COLORS.textSecondary, + }, + errorText: { + color: COLORS.error, + fontSize: 16, + textAlign: "center", + }, button: { backgroundColor: COLORS.buttonGradientStart, paddingHorizontal: 24, paddingVertical: 12, borderRadius: 8, + marginTop: 20, }, buttonText: { color: COLORS.buttonText, diff --git a/screens/LoginScreen.js b/screens/LoginScreen.js index c76d275..c32e417 100644 --- a/screens/LoginScreen.js +++ b/screens/LoginScreen.js @@ -1,5 +1,6 @@ import Ionicons from "@expo/vector-icons/Ionicons"; import { useNavigation } from "@react-navigation/native"; +import * as SecureStore from "expo-secure-store"; import { Formik } from "formik"; import { Alert, @@ -11,31 +12,44 @@ import { View, } from "react-native"; import Icon from "react-native-vector-icons/MaterialIcons"; -import Background from "../components/Background"; -import ImageBackgroundWrapper from "../components/ImageBackgroundWrapper"; -import InputField from "../components/InputField"; -import PrimaryButton from "../components/PrimaryButton"; -import SecondaryButton from "../components/SecondaryButton"; -import { H1, LinkText, P } from "../components/Typography"; +import { + Background, + H1, + ImageBackgroundWrapper, + InputField, + LinkText, + P, + PrimaryButton, + SecondaryButton, + Spinner, +} from "../components"; import { COLORS } from "../constants/colors"; +import useLogin from "../hooks/useLogin"; +import { useAuthStore } from "../store"; import { useAppContext } from "../contexts/AppContext"; import { loginValidationSchema } from "../validation/loginValidation"; const LoginScreen = () => { const navigation = useNavigation(); - const { navigateToAuth } = useAppContext(); + const { mutate, isPending } = useLogin(); + const { setAuth } = useAuthStore(); + const { navigateToMain } = useAppContext(); - const handleLogin = async (values, { setSubmitting }) => { - // values contains email and password - setSubmitting(true); - // Simulate login process - setTimeout(() => { - setSubmitting(false); - // After successful login, update the app state to show main app - // and navigate to the main app with bottom tabs - navigateToAuth(); - // The app context will ensure the user stays in the authenticated flow - }, 1500); + const handleLogin = async (values, { resetForm }) => { + mutate( + { + email: values.email, + password: values.password, + }, + { + onSuccess: async (data) => { + await SecureStore.setItemAsync("authToken", data.token); + setAuth({ user: data.user, token: data.token }); + resetForm(); + navigateToMain(); // Switch to main app state instead of navigating directly to Home + }, + }, + ); }; const handleSignUp = () => { @@ -84,7 +98,6 @@ const LoginScreen = () => { errors, touched, setFieldValue, - isSubmitting, }) => ( @@ -141,9 +154,9 @@ const LoginScreen = () => { : "Sign In"} onPress={handleSubmit} - loading={isSubmitting} + disabled={isPending} style={styles.signInButton} /> diff --git a/screens/OTPScreen.js b/screens/OTPScreen.js index a2da5ff..b63667f 100644 --- a/screens/OTPScreen.js +++ b/screens/OTPScreen.js @@ -20,7 +20,7 @@ import { useAppContext } from "../contexts/AppContext"; const OTPScreen = ({ route }) => { const navigation = useNavigation(); - const { navigateToAuth } = useAppContext(); + const { navigateToMain } = useAppContext(); const [otp, setOtp] = useState(["", "", "", ""]); const [isLoading, setIsLoading] = useState(false); const [timer, setTimer] = useState(30); @@ -66,7 +66,7 @@ const OTPScreen = ({ route }) => { }; // Handle OTP submission - const handleSubmit = () => { + const handleSubmit = async () => { // Check if all OTP fields are filled if (otp.some((digit) => digit === "")) { Alert.alert("Error", "Please enter the complete OTP"); @@ -74,14 +74,25 @@ const OTPScreen = ({ route }) => { } setIsLoading(true); - // Simulate OTP verification - setTimeout(() => { + + try { + // In a real app, you would verify the OTP with your backend + // For now, simulate successful OTP verification by logging in + // You might need to store the email/password temporarily or have a different flow + // depending on your OTP implementation + + // For demonstration purposes, we'll just navigate to main + // In a real app, you would call an API to verify the OTP + // and then get the user's token to set in the auth store + navigateToMain(); + } catch (error) { + Alert.alert( + "Verification Error", + error.message || "An error occurred during OTP verification", + ); + } finally { setIsLoading(false); - // After successful OTP verification, update the app state to show main app - // and navigate to the main app with bottom tabs - navigateToAuth(); - // The app context will ensure the user stays in the authenticated flow - }, 1500); + } }; // Handle resend OTP diff --git a/screens/RegisterScreen.js b/screens/RegisterScreen.js index a62d375..8b1a8fb 100644 --- a/screens/RegisterScreen.js +++ b/screens/RegisterScreen.js @@ -1,4 +1,7 @@ +import Ionicons from "@expo/vector-icons/Ionicons"; import { useNavigation } from "@react-navigation/native"; +import axios from "axios"; +import { Formik } from "formik"; import { Alert, KeyboardAvoidingView, @@ -7,34 +10,61 @@ import { StyleSheet, View, } from "react-native"; -import { Formik } from "formik"; import Icon from "react-native-vector-icons/MaterialIcons"; -import Ionicons from "@expo/vector-icons/Ionicons"; -import Background from "../components/Background"; -import ImageBackgroundWrapper from "../components/ImageBackgroundWrapper"; -import InputField from "../components/InputField"; -import PrimaryButton from "../components/PrimaryButton"; -import SecondaryButton from "../components/SecondaryButton"; -import { H1, LinkText, P } from "../components/Typography"; +import { + Background, + H1, + ImageBackgroundWrapper, + InputField, + LinkText, + P, + PrimaryButton, + SecondaryButton, + Spinner, +} from "../components"; import { COLORS } from "../constants/colors"; -import { useAppContext } from "../contexts/AppContext"; +import useRegister from "../hooks/useRegister"; import { registerValidationSchema } from "../validation/registerValidation"; const RegisterScreen = () => { const navigation = useNavigation(); - const { navigateToAuth } = useAppContext(); + const { mutate, isPending } = useRegister(); - const handleRegister = async (values, { setSubmitting, resetForm }) => { - setSubmitting(true); - // Simulate registration process - setTimeout(() => { - setSubmitting(false); - // After successful registration, update the app state to show main app - // and navigate to the main app with bottom tabs - navigateToAuth(); - resetForm(); - // The app context will ensure the user stays in the authenticated flow - }, 1500); + const handleRegister = (values, { resetForm }) => { + mutate( + { + email: values.email, + password: values.password, + name: values.name, + }, + { + onSuccess: () => { + Alert.alert( + "Registration Successful", + "A confirmation email has been sent to your email address. Please verify your email before logging in.", + [ + { + text: "OK", + onPress: () => { + resetForm(); + navigation.replace("Login"); + }, + }, + ], + ); + }, + onError: (error) => { + if (axios.isAxiosError(error)) { + Alert.alert( + "Registration Failed", + error.response?.data?.message || "Server error", + ); + } else { + Alert.alert("Registration Failed", "Something went wrong"); + } + }, + }, + ); }; const handleBackToLogin = () => { @@ -77,7 +107,6 @@ const RegisterScreen = () => { errors, touched, setFieldValue, - isSubmitting, }) => ( @@ -178,9 +207,9 @@ const RegisterScreen = () => { : "Sign Up"} onPress={handleSubmit} - loading={isSubmitting} + loading={isPending} style={styles.signUpButton} /> @@ -224,6 +253,10 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + mainContainer: { + flex: 1, + justifyContent: "space-between", + }, imageBackground: { flex: 1, }, diff --git a/screens/UserProfileScreen.js b/screens/UserProfileScreen.js index 781c790..ae188c9 100644 --- a/screens/UserProfileScreen.js +++ b/screens/UserProfileScreen.js @@ -1,101 +1,85 @@ import { useNavigation } from "@react-navigation/native"; import { useState } from "react"; -import { ScrollView, StyleSheet, TouchableOpacity, View } from "react-native"; +import { ScrollView, StyleSheet, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import Icon from "react-native-vector-icons/Ionicons"; -import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; -import Background from "../components/Background"; -import CommonHeader from "../components/CommonHeader"; -import SystemUIManager from "../components/SystemUIManager"; -import { H1, P } from "../components/Typography"; +import { + Background, + CommonHeader, + OutlineButton, + P, + ProfileCard, + SettingsList, + SystemUIManager, +} from "../components"; import { COLORS } from "../constants/colors"; +import useProfile from "../hooks/useProfile"; -// Profile Card Component -const ProfileCard = ({ name, phone, location, onEditAvatar }) => { - return ( - - - - - - - - - - - - - -

{name}

-

{phone}

-

{location}

-
- ); -}; +const UserProfileScreen = () => { + const navigation = useNavigation(); -// Settings List Item Component -const SettingsListItem = ({ icon, label, onPress }) => { - return ( - - - - - - -

{label}

- - - -
- ); -}; + const { data, isLoading } = useProfile(); -// Outline Button Component -const OutlineButton = ({ title, onPress, icon }) => { - return ( - - - - -

{title}

-
- ); -}; + if (isLoading) { + return ( + + + + + +

Loading profile...

+
+
+
+ ); + } -const UserProfileScreen = () => { - const navigation = useNavigation(); - const [userData] = useState({ - name: "Aarav Sharma", - phone: "+91 98765 43210", - location: "Mumbai, India", - }); + const user = data?.user || data; + + const userData = { + name: user?.name || "", + phone: user?.phoneNumber || "", + location: + user?.city && user?.country + ? `${user.city}, ${user.country}` + : "Location not set", + avatar: user?.avatar, + }; const handleEditAvatar = () => { // Handle avatar editing logic - console.log("Edit avatar pressed"); }; const handleSettingsItemPress = (item) => { - console.log(`${item} pressed`); // Navigate to specific setting screen based on item if (item === "Edit Profile") { navigation.navigate("EditProfile"); } }; - const handleLogout = () => { - // Handle logout logic - console.log("Logout pressed"); - }; + const handleLogout = () => {}; + + const settingsItems = [ + { + icon: "credit-card", + label: "Edit Profile", + onPress: () => handleSettingsItemPress("Edit Profile"), + }, + { + icon: "temple-buddhist", + label: "Saved Heritage Sites", + onPress: () => handleSettingsItemPress("Saved Heritage Sites"), + }, + { + icon: "notification-clear-all", + label: "Notifications", + onPress: () => handleSettingsItemPress("Notifications"), + }, + { + icon: "help-circle", + label: "Help & Support", + onPress: () => handleSettingsItemPress("Help & Support"), + }, + ]; return ( @@ -112,6 +96,10 @@ const UserProfileScreen = () => { name={userData.name} phone={userData.phone} location={userData.location} + avatar={ + userData.avatar || + `https://avatar.iran.liara.run/username?username=${userData.name}` + } onEditAvatar={handleEditAvatar} /> @@ -119,29 +107,7 @@ const UserProfileScreen = () => {

SETTINGS

- handleSettingsItemPress("Edit Profile")} - /> - - handleSettingsItemPress("Saved Heritage Sites")} - /> - - handleSettingsItemPress("Notifications")} - /> - - handleSettingsItemPress("Help & Support")} - /> + {/* Logout Button */} @@ -196,65 +162,6 @@ const styles = StyleSheet.create({ paddingBottom: 120, // Increased to account for FAB button and safe area paddingTop: 8, }, - profileCard: { - backgroundColor: COLORS.surface, - borderRadius: 16, - padding: 24, - alignItems: "center", - shadowColor: COLORS.shadowColor, - shadowOffset: { - width: 0, - height: 4, - }, - shadowOpacity: 0.08, - shadowRadius: 12, - elevation: 4, - marginBottom: 24, - }, - avatarContainer: { - position: "relative", - marginBottom: 16, - }, - avatarPlaceholder: { - width: 96, - height: 96, - borderRadius: 48, - backgroundColor: COLORS.inputBackground, - alignItems: "center", - justifyContent: "center", - borderWidth: 1, - borderColor: COLORS.buttonGradientEnd, - borderStyle: "solid", - }, - editButton: { - position: "absolute", - bottom: 0, - right: 0, - width: 28, - height: 28, - borderRadius: 14, - backgroundColor: COLORS.buttonGradientEnd, - alignItems: "center", - justifyContent: "center", - }, - profileName: { - fontSize: 22, - fontFamily: "PlayfairDisplayBold", - color: COLORS.textPrimary, - marginBottom: 4, - textAlign: "center", - }, - profilePhone: { - fontSize: 14, - color: COLORS.textSecondary, - marginBottom: 4, - textAlign: "center", - }, - profileLocation: { - fontSize: 13, - color: COLORS.textTertiary, - textAlign: "center", - }, sectionLabel: { fontSize: 12, fontWeight: "600", @@ -301,30 +208,18 @@ const styles = StyleSheet.create({ backgroundColor: "#EFEFEF", marginHorizontal: 16, }, - logoutButton: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - height: 48, - borderRadius: 22, - borderWidth: 1.5, - borderColor: COLORS.buttonGradientEnd, - marginBottom: 32, // Increased margin to ensure visibility above FAB - }, - logoutIcon: { - marginRight: 8, - }, - logoutText: { - fontSize: 16, - fontWeight: "600", - color: COLORS.buttonGradientEnd, - }, footerText: { fontSize: 12, color: COLORS.textPlaceholder, textAlign: "center", fontFamily: "PlayfairDisplayItalic", }, + loadingContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, }); export default UserProfileScreen; diff --git a/services/api.js b/services/api.js new file mode 100644 index 0000000..be9f7b4 --- /dev/null +++ b/services/api.js @@ -0,0 +1,49 @@ +import axios from "axios"; +import * as SecureStore from "expo-secure-store"; +import { useAuthStore } from "../store"; + +const BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL; + +const api = axios.create({ + baseURL: `${BASE_URL}/api`, + timeout: 10000, // max wait period for api response + headers: { + "Content-Type": "application/json", + }, +}); + +/** + * interceptors.request + * Runs before the request is sent. + * Attach auth token, Add headers once, Log requests, Cancel/modify requests centrally + */ +api.interceptors.request.use( + async (config) => { + const token = await SecureStore.getItemAsync("authToken"); + + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + + return config; + }, + (error) => Promise.reject(error), +); + +/** + * interceptors.response + * Runs after you get a response (success or error). + * Handle 401, Normalize API responses, Global error handling, Refresh tokens (advanced) + */ +api.interceptors.response.use( + (response) => response, + async (error) => { + if (error?.response?.status === 401) { + await SecureStore.deleteItemAsync("authToken"); + useAuthStore.getState().clearAuth(); // 🔥 force logout + } + return Promise.reject(error); + }, +); + +export default api; diff --git a/services/auth.api.js b/services/auth.api.js new file mode 100644 index 0000000..bf9cc26 --- /dev/null +++ b/services/auth.api.js @@ -0,0 +1,21 @@ +import api from "./api"; + +export const loginApi = async ({ email, password }) => { + const res = await api.post("/auth/login", { email, password }); + return res.data; +}; + +export const registerApi = async (payload) => { + const res = await api.post("/auth/register", payload); + return res.data; +}; + +export const fetchProfileApi = async () => { + const res = await api.get("/auth/profile"); + return res.data; +}; + +export const updateProfileApi = async (profileData) => { + const res = await api.put("/users/update", profileData); + return res.data; +}; diff --git a/store/authStore.js b/store/authStore.js new file mode 100644 index 0000000..bcd0335 --- /dev/null +++ b/store/authStore.js @@ -0,0 +1,50 @@ +import * as SecureStore from "expo-secure-store"; +import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; + +const secureStorage = { + getItem: async (name) => { + return await SecureStore.getItemAsync(name); + }, + setItem: async (name, value) => { + await SecureStore.setItemAsync(name, value); + }, + removeItem: async (name) => { + await SecureStore.deleteItemAsync(name); + }, +}; + +const useAuthStore = create( + persist( + (set) => ({ + user: null, + token: null, + isAuthenticated: false, + + setAuth: ({ user, token }) => + set({ + user, + token, + isAuthenticated: true, + }), + + clearAuth: () => + set({ + user: null, + token: null, + isAuthenticated: false, + }), + }), + { + name: "auth-storage", + storage: createJSONStorage(() => secureStorage), + partialize: (state) => ({ + user: state.user, + token: state.token, + isAuthenticated: state.isAuthenticated, + }), + }, + ), +); + +export default useAuthStore; diff --git a/store/index.js b/store/index.js new file mode 100644 index 0000000..ce6e746 --- /dev/null +++ b/store/index.js @@ -0,0 +1 @@ +export { default as useAuthStore } from "./authStore"; diff --git a/utils/handleApiError.js b/utils/handleApiError.js new file mode 100644 index 0000000..0287079 --- /dev/null +++ b/utils/handleApiError.js @@ -0,0 +1,43 @@ +import axios from "axios"; +import { Alert } from "react-native"; + +function handleApiError(error, options = {}) { + console.log("error :: ", error); + + if (!axios.isAxiosError(error)) { + Alert.alert("Error", "Something went wrong"); + return; + } + + const status = error.response?.status; + const message = error.response?.data?.message; + + // Email not verified + if (status === 400 && message?.toLowerCase().includes("verify")) { + Alert.alert( + "Email Not Verified", + "Please verify your email before logging in.", + options.onVerify + ? [{ text: "OK" }, { text: "Resend", onPress: options.onVerify }] + : [{ text: "OK" }], + ); + return; + } + + // Unauthorized (wrong password) + if (status === 401) { + Alert.alert("Login Failed", "Incorrect password"); + return; + } + + // Invalid credentials / bad request + if (status === 400) { + Alert.alert("Login Failed", message || "Invalid credentials"); + return; + } + + // Fallback + Alert.alert("Error", message || "Server error"); +} + +export default handleApiError;