emirapp-cli
Version:
A CLI tool to scaffold modern Expo React Native applications with authentication, tab navigation, and TypeScript
773 lines (716 loc) • 19.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.reactNativeExpoSimpleDependencies = exports.reactNativeExpoSimpleTemplates = void 0;
// React Native Expo Simple Template
exports.reactNativeExpoSimpleTemplates = {
// 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,
},
});`,
"app/(auth)/otp.tsx": `import { SafeAreaScreenComponent } from "../../src/components/ui";
import { useAuth } from "../../src/context/providers/AuthProvider";
import { router } from "expo-router";
import React, { useState, useRef, useEffect } 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 { verifyOTP } = 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 verification code");
return;
}
setIsLoading(true);
try {
const isValid = await verifyOTP(otpString);
if (isValid) {
router.replace("/(protected)" as any);
} else {
Alert.alert("Error", "Invalid verification code. Please try again.");
setOtp(["", "", "", "", "", ""]);
inputRefs.current[0]?.focus();
}
} catch (error) {
Alert.alert("Error", "Something went wrong. Please try again.");
} finally {
setIsLoading(false);
}
};
const handleResendOTP = () => {
Alert.alert("Code Sent", "A new verification code has been sent to your phone.");
};
useEffect(() => {
inputRefs.current[0]?.focus();
}, []);
return (
<SafeAreaScreenComponent>
<View style={styles.container}>
<View style={styles.content}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Enter Verification Code</Text>
<Text style={styles.subtitle}>
We've sent a 6-digit code to your phone number
</Text>
</View>
{/* OTP Input */}
<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}
selectTextOnFocus
/>
))}
</View>
{/* Verify Button */}
<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handleVerifyOTP}
disabled={isLoading}
>
<Text style={styles.buttonText}>
{isLoading ? "Verifying..." : "Verify Code"}
</Text>
</TouchableOpacity>
{/* Resend Code */}
<TouchableOpacity style={styles.resendButton} onPress={handleResendOTP}>
<Text style={styles.resendText}>Didn't receive the code? Resend</Text>
</TouchableOpacity>
</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,
},
otpContainer: {
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 30,
paddingHorizontal: 20,
},
otpInput: {
width: 45,
height: 56,
borderWidth: 1,
borderColor: "#ddd",
borderRadius: 12,
backgroundColor: "#ffffff",
textAlign: "center",
fontSize: 20,
fontWeight: "600",
color: "#333",
},
otpInputFilled: {
borderColor: "#007AFF",
backgroundColor: "#f0f8ff",
},
button: {
height: 56,
backgroundColor: "#007AFF",
borderRadius: 12,
justifyContent: "center",
alignItems: "center",
marginBottom: 20,
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: {
alignItems: "center",
paddingVertical: 12,
},
resendText: {
fontSize: 16,
color: "#007AFF",
fontWeight: "500",
},
});`,
// Protected routes
"app/(protected)/_layout.tsx": `import { Stack } from "expo-router";
export default function ProtectedLayout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ headerShown: false }} />
</Stack>
);
}`,
"app/(protected)/index.tsx": `import { SafeAreaScreenComponent } from "../../src/components/ui";
import { useAuth } from "../../src/context/providers/AuthProvider";
import React from "react";
import {
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
export default function HomeScreen() {
const { user, logout } = useAuth();
const handleLogout = async () => {
await logout();
};
return (
<SafeAreaScreenComponent>
<View style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>Welcome!</Text>
<Text style={styles.subtitle}>
You're successfully logged in with {user?.phoneNumber}
</Text>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutButtonText}>Logout</Text>
</TouchableOpacity>
</View>
</View>
</SafeAreaScreenComponent>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f8f9fa",
},
content: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 24,
},
title: {
fontSize: 32,
fontWeight: "bold",
textAlign: "center",
marginBottom: 12,
color: "#333",
},
subtitle: {
fontSize: 18,
textAlign: "center",
color: "#666",
lineHeight: 24,
marginBottom: 40,
},
logoutButton: {
paddingHorizontal: 32,
paddingVertical: 16,
backgroundColor: "#ff4444",
borderRadius: 12,
},
logoutButtonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
});`,
// Add missing component files
"src/components/ui/index.ts": `// UI Components barrel export
export { SafeAreaScreenComponent } from './SafeAreaScreenComponent';`,
"src/components/index.ts": `// Components barrel export
export * from './ui';`,
"src/components/ui/SafeAreaScreenComponent.tsx": `import React from 'react';
import { SafeAreaView, StatusBar, StyleSheet } from 'react-native';
interface SafeAreaScreenComponentProps {
children: React.ReactNode;
backgroundColor?: string;
}
export const SafeAreaScreenComponent: React.FC<SafeAreaScreenComponentProps> = ({
children,
backgroundColor = '#ffffff'
}) => {
return (
<SafeAreaView style={[styles.container, { backgroundColor }]}>
<StatusBar barStyle="dark-content" backgroundColor={backgroundColor} />
{children}
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
});`,
"src/context/providers/index.ts": `// Providers barrel export
export { ProviderComponent } from './ProviderComponent';
export { AuthProvider, useAuth } from './AuthProvider';`,
"src/context/index.ts": `// Context barrel export
export * from './providers';`,
"src/index.ts": `// Main barrel export
export * from './components';
export * from './context';`,
"src/context/providers/ProviderComponent.tsx": `import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from './AuthProvider';
interface ProviderComponentProps {
children: React.ReactNode;
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
staleTime: 5 * 60 * 1000, // 5 minutes
},
},
});
export const ProviderComponent: React.FC<ProviderComponentProps> = ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
{children}
</AuthProvider>
</QueryClientProvider>
);
};`,
"src/context/providers/AuthProvider.tsx": `import React, { createContext, useContext, useEffect, useState } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface User {
id: string;
phoneNumber: string;
isVerified: boolean;
}
interface AuthContextType {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
login: (phoneNumber: string) => Promise<void>;
logout: () => Promise<void>;
verifyOTP: (otp: string) => Promise<boolean>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const 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 const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
checkAuthState();
}, []);
const checkAuthState = async () => {
try {
const userData = await AsyncStorage.getItem('user');
if (userData) {
setUser(JSON.parse(userData));
}
} catch (error) {
console.error('Error checking auth state:', error);
} finally {
setIsLoading(false);
}
};
const login = async (phoneNumber: string) => {
try {
const newUser: User = {
id: Date.now().toString(),
phoneNumber,
isVerified: false,
};
await AsyncStorage.setItem('user', JSON.stringify(newUser));
setUser(newUser);
} catch (error) {
console.error('Error during login:', error);
throw error;
}
};
const verifyOTP = async (otp: string): Promise<boolean> => {
try {
// Simulate OTP verification
if (otp === '123456') {
if (user) {
const verifiedUser = { ...user, isVerified: true };
await AsyncStorage.setItem('user', JSON.stringify(verifiedUser));
setUser(verifiedUser);
}
return true;
}
return false;
} catch (error) {
console.error('Error verifying OTP:', error);
return false;
}
};
const logout = async () => {
try {
await AsyncStorage.removeItem('user');
setUser(null);
} catch (error) {
console.error('Error during logout:', error);
}
};
const value: AuthContextType = {
user,
isLoading,
isAuthenticated: user?.isVerified ?? false,
login,
logout,
verifyOTP,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};`,
// Configuration files
"tsconfig.json": `{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
}`,
"app.json": `{
"expo": {
"name": "Simple Auth App",
"slug": "simple-auth-app",
"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"
},
"plugins": [
"expo-router"
],
"scheme": "simple-auth-app"
}
}`,
};
exports.reactNativeExpoSimpleDependencies = [
"@expo/vector-icons",
"@react-native-async-storage/async-storage",
"@react-navigation/bottom-tabs",
"@react-navigation/elements",
"@react-navigation/native",
"@tanstack/react-query",
"react-native-gesture-handler",
"react-native-reanimated",
"react-native-safe-area-context",
"react-native-screens",
"react-native-toast-message",
];