Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions mobile/app/(auth)/login.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod"

Check warning on line 1 in mobile/app/(auth)/login.tsx

View workflow job for this annotation

GitHub Actions / mobile-checks

Insert `;`
import { Link, useRouter } from "expo-router"

Check warning on line 2 in mobile/app/(auth)/login.tsx

View workflow job for this annotation

GitHub Actions / mobile-checks

Insert `;`
import { Controller, useForm } from "react-hook-form"

Check warning on line 3 in mobile/app/(auth)/login.tsx

View workflow job for this annotation

GitHub Actions / mobile-checks

Insert `;`
import { View } from "react-native"

Check warning on line 4 in mobile/app/(auth)/login.tsx

View workflow job for this annotation

GitHub Actions / mobile-checks

There should be at least one empty line between import groups
Expand All @@ -9,7 +9,7 @@

export default function LoginScreen() {
const router = useRouter()
const setToken = useAuthStore((state) => state.setToken)
const login = useAuthStore((state) => state.login)

const {
control,
Expand All @@ -26,7 +26,14 @@

const onSubmit = async ({ email }: LoginFormValues) => {
const token = `session_${email.toLowerCase()}`
setToken(token)
await login({
token,
user: {
id: "660000000000000000000001",
email,
role: "user",
},
})
router.replace("/(tabs)/discover")
}

Expand Down
11 changes: 9 additions & 2 deletions mobile/app/(auth)/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { type RegisterFormValues, registerSchema } from "../../src/validation/au

export default function RegisterScreen() {
const router = useRouter()
const setToken = useAuthStore((state) => state.setToken)
const login = useAuthStore((state) => state.login)

const {
control,
Expand All @@ -28,7 +28,14 @@ export default function RegisterScreen() {

const onSubmit = async ({ email }: RegisterFormValues) => {
const token = `session_${email.toLowerCase()}`
setToken(token)
await login({
token,
user: {
id: "660000000000000000000001",
email,
role: "user",
},
})
router.replace("/(tabs)/discover")
}

Expand Down
10 changes: 7 additions & 3 deletions mobile/app/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ import { useAuthStore } from "../../src/store/useAuthStore"

export default function ProfileScreen() {
const router = useRouter()
const user = useAuthStore((state) => state.user)
const token = useAuthStore((state) => state.token)
const logout = useAuthStore((state) => state.logout)

const onLogout = () => {
logout()
const onLogout = async () => {
await logout()
router.replace("/(auth)/login")
}

return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", gap: 12 }}>
<Text>User Profile & Wallet.</Text>
<Text>{user?.email ?? "No user loaded"}</Text>
<Text>{token ? "Token hydrated" : "No token found"}</Text>
<Pressable
onPress={onLogout}
onPress={() => void onLogout()}
style={{ backgroundColor: "#E53935", borderRadius: 10, paddingHorizontal: 14, paddingVertical: 10 }}
>
<Text style={{ color: "#fff", fontWeight: "700" }}>Logout</Text>
Expand Down
20 changes: 20 additions & 0 deletions mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
import { useEffect } from "react"
import { Stack } from "expo-router"
import { ActivityIndicator, View } from "react-native"
import { GestureHandlerRootView } from "react-native-gesture-handler"
import { useAuthStore } from "../src/store/useAuthStore"

export default function RootLayout() {
const hydrated = useAuthStore((state) => state.hydrated)
const hydrate = useAuthStore((state) => state.hydrate)

useEffect(() => {
void hydrate()
}, [hydrate])

if (!hydrated) {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<ActivityIndicator />
</View>
</GestureHandlerRootView>
)
}

return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack screenOptions={{ headerShown: false }}>
Expand Down
5 changes: 5 additions & 0 deletions mobile/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { Redirect } from "expo-router"
import { useAuthStore } from "../src/store/useAuthStore"

export default function IndexScreen() {
const hydrated = useAuthStore((state) => state.hydrated)
const token = useAuthStore((state) => state.token)

if (!hydrated) {
return null
}

return <Redirect href={token ? "/(tabs)/discover" : "/(auth)/login"} />
}
73 changes: 70 additions & 3 deletions mobile/src/store/useAuthStore.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,80 @@
import * as SecureStore from "expo-secure-store"
import { create } from "zustand"

const TOKEN_KEY = "discoverly.auth.token"
const USER_KEY = "discoverly.auth.user"

export type AuthUser = {
id: string
email: string
role: "user" | "restaurant" | "admin"
}

type AuthState = {
token: string | null
user: AuthUser | null
hydrated: boolean
hydrate: () => Promise<void>
login: (payload: { token: string; user: AuthUser }) => Promise<void>
setToken: (token: string | null) => void
logout: () => void
logout: () => Promise<void>
}

export const useAuthStore = create<AuthState>((set) => ({
token: null,
setToken: (token) => set({ token }),
logout: () => set({ token: null }),
user: null,
hydrated: false,
hydrate: async () => {
try {
const [token, userRaw] = await Promise.all([
SecureStore.getItemAsync(TOKEN_KEY),
SecureStore.getItemAsync(USER_KEY),
])

let user: AuthUser | null = null
if (userRaw) {
try {
user = JSON.parse(userRaw) as AuthUser
} catch {
user = null
}
}

set({
token: token ?? null,
user,
hydrated: true,
})
} catch {
set({
token: null,
user: null,
hydrated: true,
})
}
},
login: async ({ token, user }) => {
await Promise.all([
SecureStore.setItemAsync(TOKEN_KEY, token),
SecureStore.setItemAsync(USER_KEY, JSON.stringify(user)),
])
set({
token,
user,
})
},
setToken: (token) => {
void (async () => {
if (token) {
await SecureStore.setItemAsync(TOKEN_KEY, token)
} else {
await SecureStore.deleteItemAsync(TOKEN_KEY)
}
})()
set({ token })
},
logout: async () => {
await Promise.all([SecureStore.deleteItemAsync(TOKEN_KEY), SecureStore.deleteItemAsync(USER_KEY)])
set({ token: null, user: null })
},
}))
17 changes: 16 additions & 1 deletion mobile/src/store/useCartStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export type CartItem = {
id: string
name: string
price: number
restaurantName?: string
imageUrl?: string
quantity?: number
}

export type CartState = {
Expand All @@ -15,7 +18,19 @@ export type CartState = {

export const useCartStore = create<CartState>((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
addItem: (item) =>
set((state) => {
const existing = state.items.find((entry) => entry.id === item.id)
if (!existing) {
return { items: [...state.items, { ...item, quantity: item.quantity ?? 1 }] }
}

return {
items: state.items.map((entry) =>
entry.id === item.id ? { ...entry, quantity: (entry.quantity ?? 1) + 1 } : entry,
),
}
}),
removeItem: (id) => set((state) => ({ items: state.items.filter((item) => item.id !== id) })),
clear: () => set({ items: [] }),
}))
Loading