diff --git a/mobile/app/(auth)/_layout.tsx b/mobile/app/(auth)/_layout.tsx index e69de29..87e438f 100644 --- a/mobile/app/(auth)/_layout.tsx +++ b/mobile/app/(auth)/_layout.tsx @@ -0,0 +1,23 @@ +import { Stack } from "expo-router"; + +export default function AuthLayout() { + return ( + + + + + + + ); +} diff --git a/mobile/app/(auth)/landing.tsx b/mobile/app/(auth)/landing.tsx index e69de29..32dfb13 100644 --- a/mobile/app/(auth)/landing.tsx +++ b/mobile/app/(auth)/landing.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { View, Text, TouchableOpacity, SafeAreaView } from "react-native"; +import { useRouter } from "expo-router"; + +export default function LandingScreen() { + const router = useRouter(); + + return ( + + + {/* Simple Branding Icon */} + + 🌱 + + + + TerraDetect + + + Real-time soil monitoring and AI-powered crop recommendations. + + + + + router.push("/(auth)/login")} + activeOpacity={0.8} + className="bg-green-600 p-5 rounded-2xl items-center shadow-sm" + > + Sign In + + + router.push("/(auth)/register")} + activeOpacity={0.7} + className="bg-white border-2 border-green-600 p-5 rounded-2xl items-center" + > + + Create Account + + + + + ); +} diff --git a/mobile/app/(auth)/login.tsx b/mobile/app/(auth)/login.tsx index e69de29..4ad84fc 100644 --- a/mobile/app/(auth)/login.tsx +++ b/mobile/app/(auth)/login.tsx @@ -0,0 +1,70 @@ +import React, { useState } from "react"; +import { View, Text, TextInput, TouchableOpacity, Alert } from "react-native"; +import { useRouter } from "expo-router"; +import { api } from "../../lib/api"; +import { useAuthStore } from "../../store/authStore"; + +export default function LoginScreen() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const router = useRouter(); + const loginToStore = useAuthStore((state) => state.login); + + const handleLogin = async () => { + if (!email || !password) + return Alert.alert("Error", "Please fill in all fields"); + + setLoading(true); + try { + const data = await api.login({ email, password }); + + // Your Go backend returns { access_token, refresh_token, user: { username, device_id } } + await loginToStore( + { accessToken: data.access_token, refreshToken: data.refresh_token }, + { username: data.user.username, deviceId: data.user.device_id }, + ); + + router.replace("/(app)/dashboard"); + } catch (err: any) { + Alert.alert("Login Failed", err.message); + } finally { + setLoading(false); + } + }; + + return ( + + + TerraDetect + + + + + + + + + {loading ? "Authenticating..." : "Sign In"} + + + + + ); +} diff --git a/mobile/app/(auth)/register-success.tsx b/mobile/app/(auth)/register-success.tsx index e69de29..eda5444 100644 --- a/mobile/app/(auth)/register-success.tsx +++ b/mobile/app/(auth)/register-success.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { View, Text, TouchableOpacity } from "react-native"; +import { useRouter } from "expo-router"; + +export default function RegisterSuccessScreen() { + const router = useRouter(); + + return ( + + + ✓ + + + + You're all set! + + + Your account has been created. You can now log in to connect your + sensors and analyze your soil. + + + router.replace("/(auth)/login")} + className="bg-green-600 w-full mt-10 p-5 rounded-2xl items-center" + > + Go to Login + + + ); +} diff --git a/mobile/app/(auth)/register.tsx b/mobile/app/(auth)/register.tsx index e69de29..b7085a6 100644 --- a/mobile/app/(auth)/register.tsx +++ b/mobile/app/(auth)/register.tsx @@ -0,0 +1,111 @@ +import React, { useState } from "react"; +import { + View, + Text, + TextInput, + TouchableOpacity, + Alert, + ScrollView, +} from "react-native"; +import { useRouter } from "expo-router"; +import { api } from "../../lib/api"; + +export default function RegisterScreen() { + const [form, setForm] = useState({ + username: "", + email: "", + password: "", + deviceId: "", + }); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + const handleRegister = async () => { + if (!form.username || !form.email || !form.password) { + return Alert.alert( + "Error", + "Username, Email, and Password are required.", + ); + } + + setLoading(true); + try { + // Calling your Go backend via the api.ts utility + await api.register({ + username: form.username, + email: form.email, + password: form.password, + device_id: form.deviceId, + }); + + // Navigate to the success screen instead of a blocking alert + router.push("/(auth)/register-success"); + } catch (err: any) { + // Fallback to error message from your Gin middleware/handlers + Alert.alert("Registration Failed", err.message); + } finally { + setLoading(false); + } + }; + + return ( + + + Create Account + + + Join TerraDetect to start monitoring your soil. + + + + setForm({ ...form, username: val })} + /> + setForm({ ...form, email: val })} + /> + setForm({ ...form, password: val })} + /> + setForm({ ...form, deviceId: val })} + /> + + + + {loading ? "Creating Account..." : "Sign Up"} + + + + router.push("/(auth)/login")} + className="py-4" + > + + Already have an account?{" "} + Login + + + + + ); +} diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index e69de29..ed0b08b 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from "react"; +import { Stack, useRouter, useSegments } from "expo-router"; +import { useAuthStore } from "../store/authStore"; +import { View, ActivityIndicator } from "react-native"; + +export default function RootLayout() { + const { accessToken, loadFromStorage } = useAuthStore(); + const segments = useSegments(); + const router = useRouter(); + const [isReady, setIsReady] = useState(false); + + // 1. Initialize Auth State + useEffect(() => { + const initialize = async () => { + await loadFromStorage(); + setIsReady(true); + }; + initialize(); + }, []); + + // 2. Auth Guard Logic + useEffect(() => { + if (!isReady) return; + + const inAppGroup = segments[0] === "(app)"; + + if (!accessToken && inAppGroup) { + // If user is not logged in and tries to access (app) folder + router.replace("/(auth)/landing"); + } else if (accessToken && !inAppGroup) { + // If user is logged in and tries to access (auth) folder + router.replace("/(app)/dashboard"); + } + }, [accessToken, segments, isReady]); + + // 3. Loading State (Prevents flicker during hydration) + if (!isReady) { + return ( + + + + ); + } + + return ( + + + + + ); +} diff --git a/mobile/babel.config.js b/mobile/babel.config.js new file mode 100644 index 0000000..f3c649b --- /dev/null +++ b/mobile/babel.config.js @@ -0,0 +1,9 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: [ + ["babel-preset-expo", { jsxImportSource: "nativewind" }], + "nativewind/babel", + ], + }; +}; diff --git a/mobile/global.css b/mobile/global.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/mobile/global.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/mobile/nativewind-env.d.ts b/mobile/nativewind-env.d.ts new file mode 100644 index 0000000..a13e313 --- /dev/null +++ b/mobile/nativewind-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/mobile/tailwind.config.js b/mobile/tailwind.config.js new file mode 100644 index 0000000..6288219 --- /dev/null +++ b/mobile/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"], + presets: [require("nativewind/preset")], + theme: { + extend: {}, + }, + plugins: [], +};