UNPKG

emirapp-cli

Version:

A CLI tool to scaffold modern Expo React Native applications with authentication, tab navigation, and TypeScript

1,402 lines (1,324 loc) 35.5 kB
"use strict"; // Template files for the Expo React Native app Object.defineProperty(exports, "__esModule", { value: true }); exports.screenTemplates = exports.configTemplates = exports.componentTemplates = exports.additionalTemplates = exports.srcTemplates = exports.appTemplates = void 0; exports.appTemplates = { // App layout files "app/_layout.tsx": `import { Stack } from "expo-router"; import { ProviderComponent } from "@/src/context/providers/ProviderComponent"; export default function RootLayout() { return ( <ProviderComponent> <Stack screenOptions={{ headerShown: false }}> <Stack.Screen name="(auth)" options={{ headerShown: false }} /> <Stack.Screen name="(protected)" options={{ headerShown: false }} /> </Stack> </ProviderComponent> ); }`, "app/+not-found.tsx": `import { Link, Stack } from "expo-router"; import { StyleSheet, Text, View } from "react-native"; export default function NotFoundScreen() { return ( <> <Stack.Screen options={{ title: "Oops!" }} /> <View style={styles.container}> <Text style={styles.title}>This screen doesn't exist.</Text> <Link href="/" style={styles.link}> <Text style={styles.linkText}>Go to home screen!</Text> </Link> </View> </> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", justifyContent: "center", padding: 20, }, title: { fontSize: 20, fontWeight: "bold", }, link: { marginTop: 15, paddingVertical: 15, }, linkText: { fontSize: 14, color: "#2e78b7", }, });`, // Auth layout and screens "app/(auth)/_layout.tsx": `import { Stack } from "expo-router"; export default function AuthLayout() { return ( <Stack screenOptions={{ headerShown: false }}> <Stack.Screen name="index" options={{ headerShown: false }} /> <Stack.Screen name="otp" options={{ headerShown: false }} /> </Stack> ); }`, "app/(auth)/index.tsx": `import { SafeAreaScreenComponent } from "@/src/components/ui"; import { router } from "expo-router"; import React, { useState } from "react"; import { Alert, StyleSheet, Text, TextInput, TouchableOpacity, View, } from "react-native"; export default function AuthScreen() { const [phoneNumber, setPhoneNumber] = useState(""); const [isLoading, setIsLoading] = useState(false); const handleSendOTP = async () => { if (!phoneNumber.trim()) { Alert.alert("Error", "Please enter your phone number"); return; } if (phoneNumber.length < 10) { Alert.alert("Error", "Please enter a valid phone number"); return; } setIsLoading(true); // Simulate API call setTimeout(() => { setIsLoading(false); // Navigate to OTP screen with phone number router.push("/(auth)/otp" as any); }, 1000); }; return ( <SafeAreaScreenComponent> <View style={styles.container}> <View style={styles.content}> {/* Header */} <View style={styles.header}> <Text style={styles.title}>Enter Your Phone Number</Text> <Text style={styles.subtitle}> We'll send you a verification code to confirm your number </Text> </View> {/* Phone Input Form */} <View style={styles.form}> <View style={styles.phoneInputContainer}> <View style={styles.countryCode}> <Text style={styles.countryCodeText}>+1</Text> </View> <TextInput style={styles.phoneInput} placeholder="(555) 123-4567" value={phoneNumber} onChangeText={setPhoneNumber} keyboardType="phone-pad" maxLength={15} /> </View> <TouchableOpacity style={[styles.button, isLoading && styles.buttonDisabled]} onPress={handleSendOTP} disabled={isLoading} > <Text style={styles.buttonText}> {isLoading ? "Sending..." : "Send Verification Code"} </Text> </TouchableOpacity> </View> {/* Footer */} <View style={styles.footer}> <Text style={styles.footerText}> By continuing, you agree to our Terms of Service and Privacy Policy </Text> </View> </View> </View> </SafeAreaScreenComponent> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#f8f9fa", }, content: { flex: 1, justifyContent: "center", paddingHorizontal: 24, }, header: { alignItems: "center", marginBottom: 40, }, title: { fontSize: 28, fontWeight: "bold", textAlign: "center", marginBottom: 12, color: "#333", }, subtitle: { fontSize: 16, textAlign: "center", color: "#666", lineHeight: 22, }, form: { gap: 20, }, phoneInputContainer: { flexDirection: "row", alignItems: "center", borderWidth: 1, borderColor: "#ddd", borderRadius: 12, backgroundColor: "#ffffff", overflow: "hidden", }, countryCode: { paddingHorizontal: 16, paddingVertical: 18, backgroundColor: "#f8f9fa", borderRightWidth: 1, borderRightColor: "#ddd", }, countryCodeText: { fontSize: 16, fontWeight: "600", color: "#333", }, phoneInput: { flex: 1, height: 56, paddingHorizontal: 16, fontSize: 16, color: "#333", }, button: { height: 56, backgroundColor: "#007AFF", borderRadius: 12, justifyContent: "center", alignItems: "center", marginTop: 8, shadowColor: "#007AFF", shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 3, }, buttonDisabled: { backgroundColor: "#B0B0B0", opacity: 0.7, }, buttonText: { color: "white", fontSize: 18, fontWeight: "600", }, footer: { marginTop: 40, alignItems: "center", }, footerText: { fontSize: 14, color: "#999", textAlign: "center", lineHeight: 20, }, });`, }; exports.srcTemplates = { // Context providers "src/context/providers/ProviderComponent.tsx": `import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import React from "react"; import { AuthProvider } from "./AuthProvider"; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // 5 minutes gcTime: 1000 * 60 * 10, // 10 minutes (formerly cacheTime) }, }, }); interface ProviderComponentProps { children: React.ReactNode; } export function ProviderComponent({ children }: ProviderComponentProps) { return ( <QueryClientProvider client={queryClient}> <AuthProvider>{children}</AuthProvider> </QueryClientProvider> ); }`, "src/context/providers/AuthProvider.tsx": `import React, { createContext, useContext, useEffect, useState } from 'react'; import { router } from 'expo-router'; // import * as SecureStore from 'expo-secure-store'; interface AuthContextType { isAuthenticated: boolean; login: (accessToken: string, refreshToken: string) => Promise<void>; logout: () => Promise<void>; loading: boolean; } const AuthContext = createContext<AuthContextType | undefined>(undefined); export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; } interface AuthProviderProps { children: React.ReactNode; } export function AuthProvider({ children }: AuthProviderProps) { const [isAuthenticated, setIsAuthenticated] = useState(false); const [loading, setLoading] = useState(true); // Mock tokens for development - uncomment SecureStore usage when ready const mockAccessToken = "mock_access_token_123"; const mockRefreshToken = "mock_refresh_token_456"; const checkAuthStatus = async () => { try { setLoading(true); // Commented out SecureStore implementation - uncomment when ready // const accessToken = await SecureStore.getItemAsync('access_token'); // const refreshToken = await SecureStore.getItemAsync('refresh_token'); // Mock implementation const accessToken = mockAccessToken; const refreshToken = mockRefreshToken; if (accessToken && refreshToken) { setIsAuthenticated(true); router.replace("/(protected)/main/home" as any); } else { setIsAuthenticated(false); router.replace("/(auth)" as any); } } catch (error) { console.error("Auth check error:", error); setIsAuthenticated(false); router.replace("/(auth)" as any); } finally { setLoading(false); } }; const login = async (accessToken: string, refreshToken: string) => { try { // Commented out SecureStore implementation - uncomment when ready // await SecureStore.setItemAsync('access_token', accessToken); // await SecureStore.setItemAsync('refresh_token', refreshToken); setIsAuthenticated(true); router.replace("/(protected)/main/home" as any); } catch (error) { console.error("Login error:", error); } }; const logout = async () => { try { // Commented out SecureStore implementation - uncomment when ready // await SecureStore.deleteItemAsync('access_token'); // await SecureStore.deleteItemAsync('refresh_token'); setIsAuthenticated(false); router.replace("/(auth)" as any); } catch (error) { console.error("Logout error:", error); } }; useEffect(() => { checkAuthStatus(); }, []); const value: AuthContextType = { isAuthenticated, login, logout, loading, }; return ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider> ); }`, }; // Additional templates that are missing exports.additionalTemplates = { "app/(auth)/otp.tsx": `import { SafeAreaScreenComponent } from "@/src/components/ui"; import { useAuth } from "@/src/context/providers/AuthProvider"; import { Ionicons } from "@expo/vector-icons"; import { router } from "expo-router"; import React, { useRef, useState } from "react"; import { Alert, StyleSheet, Text, TextInput, TouchableOpacity, View, } from "react-native"; export default function OtpScreen() { const [otp, setOtp] = useState(["", "", "", "", "", ""]); const [isLoading, setIsLoading] = useState(false); const { login } = useAuth(); const inputRefs = useRef<TextInput[]>([]); const handleOtpChange = (value: string, index: number) => { const newOtp = [...otp]; newOtp[index] = value; setOtp(newOtp); // Auto-focus next input if (value && index < 5) { inputRefs.current[index + 1]?.focus(); } }; const handleKeyPress = (key: string, index: number) => { if (key === "Backspace" && !otp[index] && index > 0) { inputRefs.current[index - 1]?.focus(); } }; const handleVerifyOtp = async () => { const otpString = otp.join(""); if (otpString.length !== 6) { Alert.alert("Error", "Please enter the complete 6-digit code"); return; } setIsLoading(true); // Simulate API call setTimeout(async () => { try { // Mock OTP verification - replace with actual API call await login("mock_access_token", "mock_refresh_token"); } catch (error) { Alert.alert("Error", "Invalid verification code. Please try again."); } finally { setIsLoading(false); } }, 1000); }; const handleResendCode = () => { Alert.alert( "Code Sent", "A new verification code has been sent to your phone." ); }; return ( <SafeAreaScreenComponent> <View style={styles.container}> <View style={styles.content}> {/* Header */} <View style={styles.header}> <TouchableOpacity style={styles.backButton} onPress={() => router.back()} > <Ionicons name="arrow-back" size={24} color="#333" /> </TouchableOpacity> <Text style={styles.title}>Verify Your Phone</Text> <Text style={styles.subtitle}> Enter the 6-digit code sent to your phone number </Text> </View> {/* OTP Input */} <View style={styles.form}> <View style={styles.otpContainer}> {otp.map((digit, index) => ( <TextInput key={index} ref={(ref) => { if (ref) inputRefs.current[index] = ref; }} style={[ styles.otpInput, digit ? styles.otpInputFilled : null, ]} value={digit} onChangeText={(value) => handleOtpChange(value, index)} onKeyPress={({ nativeEvent }) => handleKeyPress(nativeEvent.key, index) } keyboardType="numeric" maxLength={1} textAlign="center" /> ))} </View> <TouchableOpacity style={[styles.button, isLoading && styles.buttonDisabled]} onPress={handleVerifyOtp} disabled={isLoading} > <Text style={styles.buttonText}> {isLoading ? "Verifying..." : "Verify Code"} </Text> </TouchableOpacity> <TouchableOpacity style={styles.resendButton} onPress={handleResendCode} > <Text style={styles.resendText}> Didn't receive the code?{" "} <Text style={styles.resendLink}>Resend</Text> </Text> </TouchableOpacity> </View> </View> </View> </SafeAreaScreenComponent> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#f8f9fa", }, content: { flex: 1, paddingHorizontal: 24, paddingTop: 60, }, header: { alignItems: "center", marginBottom: 40, }, backButton: { position: "absolute", left: 0, top: 0, padding: 8, }, title: { fontSize: 28, fontWeight: "bold", textAlign: "center", marginBottom: 12, color: "#333", marginTop: 40, }, subtitle: { fontSize: 16, textAlign: "center", color: "#666", lineHeight: 22, }, form: { gap: 24, }, otpContainer: { flexDirection: "row", justifyContent: "space-between", marginBottom: 20, }, otpInput: { width: 50, height: 60, borderWidth: 2, borderColor: "#e0e0e0", borderRadius: 12, fontSize: 24, fontWeight: "600", backgroundColor: "#ffffff", color: "#333", }, otpInputFilled: { borderColor: "#007AFF", backgroundColor: "#f0f8ff", }, button: { height: 56, backgroundColor: "#007AFF", borderRadius: 12, justifyContent: "center", alignItems: "center", shadowColor: "#007AFF", shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 3, }, buttonDisabled: { backgroundColor: "#B0B0B0", opacity: 0.7, }, buttonText: { color: "white", fontSize: 18, fontWeight: "600", }, resendButton: { marginTop: 20, alignItems: "center", padding: 12, }, resendText: { fontSize: 16, color: "#666", }, resendLink: { color: "#007AFF", fontWeight: "600", }, });`, // Protected layout "app/(protected)/_layout.tsx": `import { Stack } from "expo-router"; export default function ProtectedLayout() { return ( <Stack screenOptions={{ headerShown: false }}> <Stack.Screen name="main" options={{ headerShown: false }} /> </Stack> ); }`, // Main tab layout "app/(protected)/main/_layout.tsx": `import { Ionicons } from "@expo/vector-icons"; import { Tabs } from "expo-router"; import React from "react"; import { Platform } from "react-native"; export default function MainLayout() { return ( <Tabs screenOptions={{ headerShown: false, tabBarActiveTintColor: "#007AFF", tabBarInactiveTintColor: "#8E8E93", tabBarStyle: { backgroundColor: "#FFFFFF", borderTopWidth: 1, borderTopColor: "#E5E5EA", paddingBottom: Platform.OS === "ios" ? 20 : 10, paddingTop: 10, height: Platform.OS === "ios" ? 90 : 70, shadowColor: "#000", shadowOffset: { width: 0, height: -2, }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 8, }, tabBarLabelStyle: { fontSize: 12, fontWeight: "600", marginTop: 4, }, tabBarIconStyle: { marginBottom: 2, }, }} > <Tabs.Screen name="home" options={{ title: "Home", tabBarIcon: ({ color, size, focused }) => ( <Ionicons name={focused ? "home" : "home-outline"} size={size} color={color} /> ), }} /> <Tabs.Screen name="explore" options={{ title: "Explore", tabBarIcon: ({ color, size, focused }) => ( <Ionicons name={focused ? "compass" : "compass-outline"} size={size} color={color} /> ), }} /> <Tabs.Screen name="profile" options={{ title: "Profile", tabBarIcon: ({ color, size, focused }) => ( <Ionicons name={focused ? "person" : "person-outline"} size={size} color={color} /> ), }} /> </Tabs> ); }`, }; // Additional templates for components and screens exports.componentTemplates = { "src/components/ui/SafeAreaScreenComponent.tsx": `import React from 'react'; import { View, StyleSheet, ViewStyle } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; interface SafeAreaScreenComponentProps { children: React.ReactNode; style?: ViewStyle; backgroundColor?: string; edges?: ('top' | 'right' | 'bottom' | 'left')[]; } export function SafeAreaScreenComponent({ children, style, backgroundColor = '#ffffff', edges = ['top', 'bottom', 'left', 'right'] }: SafeAreaScreenComponentProps) { return ( <SafeAreaView style={[ styles.container, { backgroundColor }, style ]} edges={edges} > <View style={styles.content}> {children} </View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, }, content: { flex: 1, }, });`, "src/components/ui/index.ts": `// UI Components barrel exports export { SafeAreaScreenComponent } from "./SafeAreaScreenComponent";`, }; // Configuration templates exports.configTemplates = { "app.json": `{ "expo": { "name": "EmirApp", "slug": "emirapp", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#ffffff" }, "assetBundlePatterns": [ "**/*" ], "ios": { "supportsTablet": true }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" } }, "web": { "favicon": "./assets/favicon.png", "bundler": "metro" }, "scheme": "emirapp", "plugins": [ "expo-router" ], "experiments": { "typedRoutes": true } } }`, "metro.config.js": `const { getDefaultConfig } = require('expo/metro-config'); const config = getDefaultConfig(__dirname); module.exports = config;`, "babel.config.js": `module.exports = function (api) { api.cache(true); return { presets: ['babel-preset-expo'], plugins: ['expo-router/babel'], }; };`, }; // Screen templates exports.screenTemplates = { "app/(protected)/main/home/_layout.tsx": `import { Stack } from "expo-router"; export default function HomeLayout() { return ( <Stack screenOptions={{ headerShown: false }}> <Stack.Screen name="index" options={{ headerShown: false }} /> </Stack> ); }`, "app/(protected)/main/home/index.tsx": `import { SafeAreaScreenComponent } from "@/src/components/ui"; import { Ionicons } from "@expo/vector-icons"; import { router } from "expo-router"; import React from "react"; import { ScrollView, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; export default function HomeScreen() { const handleProductPress = (id: string) => { // Navigate to product details - implement when needed console.log("Product pressed:", id); }; return ( <SafeAreaScreenComponent> <ScrollView style={styles.container} showsVerticalScrollIndicator={false}> <View style={styles.content}> {/* Welcome Header */} <View style={styles.welcomeHeader}> <View> <Text style={styles.welcomeText}>Welcome back!</Text> <Text style={styles.subtitle}>Discover amazing products</Text> </View> <Ionicons name="notifications-outline" size={24} color="#333" /> </View> <View style={styles.productGrid}> {[1, 2, 3, 4, 5, 6].map((id) => ( <TouchableOpacity key={id} style={styles.productCard} onPress={() => handleProductPress(id.toString())} > <Text style={styles.productTitle}>Product {id}</Text> <Text style={styles.productDescription}> Amazing product description here </Text> </TouchableOpacity> ))} </View> </View> </ScrollView> </SafeAreaScreenComponent> ); } const styles = StyleSheet.create({ container: { flex: 1, }, content: { padding: 24, }, welcomeHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 24, marginTop: 10, }, welcomeText: { fontSize: 28, fontWeight: "bold", color: "#333", marginBottom: 4, }, subtitle: { fontSize: 16, color: "#666", marginBottom: 16, }, productGrid: { gap: 12, }, productCard: { backgroundColor: "#f9f9f9", padding: 20, borderRadius: 12, borderWidth: 1, borderColor: "#e0e0e0", }, productTitle: { fontSize: 18, fontWeight: "600", marginBottom: 4, color: "#333", }, productDescription: { fontSize: 14, color: "#666", }, });`, "app/(protected)/main/explore/_layout.tsx": `import { Stack } from "expo-router"; export default function ExploreLayout() { return ( <Stack screenOptions={{ headerShown: false }}> <Stack.Screen name="index" options={{ headerShown: false }} /> </Stack> ); }`, "app/(protected)/main/explore/index.tsx": `import { SafeAreaScreenComponent } from "@/src/components/ui"; import { Ionicons } from "@expo/vector-icons"; import React from "react"; import { ScrollView, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; export default function ExploreScreen() { const categories = [ { id: "1", name: "Electronics", count: 245, icon: "phone-portrait-outline", }, { id: "2", name: "Fashion", count: 189, icon: "shirt-outline" }, { id: "3", name: "Home & Garden", count: 156, icon: "home-outline" }, { id: "4", name: "Sports", count: 98, icon: "fitness-outline" }, { id: "5", name: "Books", count: 234, icon: "book-outline" }, { id: "6", name: "Health", count: 67, icon: "medical-outline" }, ]; const handleCategoryPress = (categoryId: string) => { // For now, just show an alert since we don't have category pages alert(\`Category \${categoryId} selected\`); }; return ( <SafeAreaScreenComponent> <ScrollView style={styles.container} showsVerticalScrollIndicator={false}> <View style={styles.content}> {/* Explore Header */} <View style={styles.exploreHeader}> <View> <Text style={styles.title}>Explore</Text> <Text style={styles.subtitle}>Discover new categories</Text> </View> <Ionicons name="search-outline" size={24} color="#333" /> </View> <View style={styles.categoryGrid}> {categories.map((category) => ( <TouchableOpacity key={category.id} style={styles.categoryCard} onPress={() => handleCategoryPress(category.id)} activeOpacity={0.7} > <View style={styles.categoryIconContainer}> <Ionicons name={category.icon as any} size={32} color="#007AFF" /> </View> <Text style={styles.categoryName}>{category.name}</Text> <Text style={styles.categoryCount}>{category.count} items</Text> </TouchableOpacity> ))} </View> </View> </ScrollView> </SafeAreaScreenComponent> ); } const styles = StyleSheet.create({ container: { flex: 1, }, content: { padding: 24, }, exploreHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 24, marginTop: 10, }, title: { fontSize: 28, fontWeight: "bold", color: "#333", marginBottom: 4, }, subtitle: { fontSize: 16, color: "#666", marginBottom: 16, }, categoryGrid: { flexDirection: "row", flexWrap: "wrap", gap: 12, }, categoryCard: { width: "48%", backgroundColor: "#ffffff", padding: 20, borderRadius: 12, borderWidth: 1, borderColor: "#e0e0e0", alignItems: "center", shadowColor: "#000", shadowOffset: { width: 0, height: 1, }, shadowOpacity: 0.05, shadowRadius: 2, elevation: 1, }, categoryIconContainer: { width: 60, height: 60, borderRadius: 30, backgroundColor: "#f0f8ff", justifyContent: "center", alignItems: "center", marginBottom: 12, }, categoryName: { fontSize: 16, fontWeight: "600", marginBottom: 4, color: "#333", textAlign: "center", }, categoryCount: { fontSize: 12, color: "#666", }, });`, "app/(protected)/main/profile.tsx": `import { SafeAreaScreenComponent } from "@/src/components/ui"; import { useAuth } from "@/src/context/providers/AuthProvider"; import { Ionicons } from "@expo/vector-icons"; import React, { useState } from "react"; import { ActivityIndicator, Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; interface ProfileOption { id: string; title: string; subtitle: string; icon: keyof typeof Ionicons.glyphMap; action?: () => void; } interface UserProfile { name: string; email: string; initials: string; memberSince: string; } export default function ProfileScreen() { const { logout, loading } = useAuth(); const [isLoggingOut, setIsLoggingOut] = useState(false); // Mock user data - in a real app, this would come from your user context or API const userProfile: UserProfile = { name: "John Doe", email: "john.doe@example.com", initials: "JD", memberSince: "January 2024", }; const handleLogout = async () => { Alert.alert("Sign Out", "Are you sure you want to sign out?", [ { text: "Cancel", style: "cancel", }, { text: "Sign Out", style: "destructive", onPress: async () => { try { setIsLoggingOut(true); await logout(); } catch (error) { console.error("Logout error:", error); Alert.alert("Error", "Failed to sign out. Please try again."); } finally { setIsLoggingOut(false); } }, }, ]); }; const profileOptions: ProfileOption[] = [ { id: "1", title: "Edit Profile", subtitle: "Update your information", icon: "person-outline", action: () => Alert.alert( "Coming Soon", "Edit profile feature will be available soon!" ), }, { id: "2", title: "Order History", subtitle: "View your past orders", icon: "receipt-outline", action: () => Alert.alert( "Coming Soon", "Order history feature will be available soon!" ), }, { id: "3", title: "Notifications", subtitle: "Manage notifications", icon: "notifications-outline", action: () => Alert.alert( "Coming Soon", "Notification settings will be available soon!" ), }, { id: "4", title: "Privacy Settings", subtitle: "Control your privacy", icon: "shield-outline", action: () => Alert.alert("Coming Soon", "Privacy settings will be available soon!"), }, { id: "5", title: "Help & Support", subtitle: "Get help or contact us", icon: "help-circle-outline", action: () => Alert.alert("Help & Support", "Contact us at support@example.com"), }, { id: "6", title: "About", subtitle: "App version and info", icon: "information-circle-outline", action: () => Alert.alert( "About", "App Version 1.0.0\\nBuilt with React Native & Expo" ), }, ]; if (loading) { return ( <SafeAreaScreenComponent> <View style={styles.loadingContainer}> <ActivityIndicator size="large" color="#007AFF" /> <Text style={styles.loadingText}>Loading...</Text> </View> </SafeAreaScreenComponent> ); } return ( <SafeAreaScreenComponent> <ScrollView style={styles.container} showsVerticalScrollIndicator={false}> <View style={styles.content}> {/* Profile Info */} <View style={styles.profileInfo}> <View style={styles.avatar}> <Text style={styles.avatarText}>{userProfile.initials}</Text> </View> <Text style={styles.userName}>{userProfile.name}</Text> <Text style={styles.userEmail}>{userProfile.email}</Text> <Text style={styles.memberSince}> Member since {userProfile.memberSince} </Text> </View> {/* Profile Options */} <View style={styles.optionsContainer}> {profileOptions.map((option) => ( <TouchableOpacity key={option.id} style={styles.optionCard} onPress={option.action} activeOpacity={0.7} > <View style={styles.optionLeft}> <View style={styles.iconContainer}> <Ionicons name={option.icon} size={24} color="#007AFF" /> </View> <View style={styles.optionContent}> <Text style={styles.optionTitle}>{option.title}</Text> <Text style={styles.optionSubtitle}>{option.subtitle}</Text> </View> </View> <Ionicons name="chevron-forward" size={20} color="#C7C7CC" /> </TouchableOpacity> ))} </View> {/* Logout Button */} <TouchableOpacity style={[ styles.logoutButton, isLoggingOut && styles.logoutButtonDisabled, ]} onPress={handleLogout} disabled={isLoggingOut} activeOpacity={0.8} > {isLoggingOut ? ( <ActivityIndicator size="small" color="white" /> ) : ( <> <Ionicons name="log-out-outline" size={20} color="white" /> <Text style={styles.logoutText}>Sign Out</Text> </> )} </TouchableOpacity> </View> </ScrollView> </SafeAreaScreenComponent> ); } const styles = StyleSheet.create({ container: { flex: 1, }, loadingContainer: { flex: 1, justifyContent: "center", alignItems: "center", padding: 24, }, loadingText: { marginTop: 16, fontSize: 16, color: "#666", }, content: { padding: 24, }, profileInfo: { alignItems: "center", marginBottom: 32, marginTop: 20, }, avatar: { width: 80, height: 80, borderRadius: 40, backgroundColor: "#007AFF", justifyContent: "center", alignItems: "center", marginBottom: 16, shadowColor: "#000", shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, avatarText: { color: "white", fontSize: 24, fontWeight: "bold", }, userName: { fontSize: 24, fontWeight: "bold", color: "#333", marginBottom: 4, }, userEmail: { fontSize: 16, color: "#666", marginBottom: 8, }, memberSince: { fontSize: 14, color: "#999", fontStyle: "italic", }, optionsContainer: { gap: 12, marginBottom: 32, }, optionCard: { backgroundColor: "#ffffff", padding: 20, borderRadius: 12, borderWidth: 1, borderColor: "#e0e0e0", flexDirection: "row", alignItems: "center", shadowColor: "#000", shadowOffset: { width: 0, height: 1, }, shadowOpacity: 0.05, shadowRadius: 2, elevation: 1, }, optionLeft: { flex: 1, flexDirection: "row", alignItems: "center", }, iconContainer: { width: 40, height: 40, borderRadius: 20, backgroundColor: "#f0f8ff", justifyContent: "center", alignItems: "center", marginRight: 16, }, optionContent: { flex: 1, }, optionTitle: { fontSize: 16, fontWeight: "600", color: "#333", marginBottom: 4, }, optionSubtitle: { fontSize: 14, color: "#666", }, logoutButton: { backgroundColor: "#ff4444", paddingVertical: 16, paddingHorizontal: 24, borderRadius: 12, alignItems: "center", flexDirection: "row", justifyContent: "center", gap: 8, shadowColor: "#ff4444", shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 3, }, logoutButtonDisabled: { backgroundColor: "#ff8888", opacity: 0.7, }, logoutText: { color: "white", fontSize: 18, fontWeight: "600", }, });`, };