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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<p align="center">
<img src="https://img.shields.io/badge/React_Native-20232A?style=for-the-badge&logo=react&logoColor=61DAFB" alt="React Native">
<img src="https://img.shields.io/badge/Expo-000020?style=for-the-badge&logo=expo&logoColor=white" alt="Expo">
<img src="https://img.shields.io/badge/Expo_SDK-55-000020?style=for-the-badge&logo=expo&logoColor=white" alt="Expo SDK 55">
<img src="https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript">
<img src="https://img.shields.io/badge/Expo_Router-000000?style=for-the-badge&logo=expo&logoColor=white" alt="Expo Router">
<img src="https://img.shields.io/badge/NativeWind-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white" alt="NativeWind">
Expand Down Expand Up @@ -95,7 +96,7 @@ Open the app:
</h2>

- **React Native** – Cross-platform mobile framework
- **Expo SDK** – Development platform and tooling
- **Expo SDK 55** – Development platform and tooling
- **Expo Router** – File-based routing
- **TypeScript** – Type-safe development
- **NativeWind** – Tailwind CSS for React Native
Expand Down
396 changes: 151 additions & 245 deletions bun.lock

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"dependencies": {
"@expo/metro-runtime": "~55.0.6",
"@hookform/resolvers": "^5.2.2",
"@react-native-async-storage/async-storage": "3.0.1",
"@react-navigation/native": "^7.1.31",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/native": "^7.1.28",
"@rn-primitives/avatar": "^1.2.0",
"@rn-primitives/label": "^1.2.0",
"@rn-primitives/popover": "^1.2.0",
Expand All @@ -54,21 +54,21 @@
"i18next": "^25.8.13",
"lucide-react-native": "^0.575.0",
"nativewind": "^4.2.2",
"react": "19.2.4",
"react-dom": "19.2.4",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.71.2",
"react-i18next": "^16.5.4",
"react-native": "0.84.1",
"react-native-reanimated": "~4.2.2",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "~4.24.0",
"react-native": "0.83.2",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0",
"react-native-svg": "15.15.3",
"react-native-web": "^0.21.2",
"react-native-worklets": "0.7.4",
"react-native-worklets": "0.7.2",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"zod": "^4.0.0"
"zod": "^4.3.6"
},
"devDependencies": {
"@babel/core": "^7.26.0",
Expand Down
16 changes: 9 additions & 7 deletions src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Icon, Label, NativeTabs } from "expo-router/unstable-native-tabs";
import { NativeTabs } from "expo-router/unstable-native-tabs";
import { useTranslation } from "react-i18next";

export default function TabLayout() {
Expand All @@ -7,18 +7,20 @@ export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index">
<Label>{t("tabs.home")}</Label>
<Icon sf="house.fill" drawable="custom_android_drawable" />
<NativeTabs.Trigger.Label>{t("tabs.home")}</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf="house.fill" md="home" />
</NativeTabs.Trigger>

<NativeTabs.Trigger name="about">
<Icon sf="info.circle.fill" drawable="custom_about_drawable" />
<Label>{t("tabs.about")}</Label>
<NativeTabs.Trigger.Icon sf="info.circle.fill" md="info" />
<NativeTabs.Trigger.Label>{t("tabs.about")}</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>

<NativeTabs.Trigger name="settings">
<Icon sf="gear" drawable="custom_settings_drawable" />
<Label>{t("tabs.settings")}</Label>
<NativeTabs.Trigger.Icon sf="gear" md="settings" />
<NativeTabs.Trigger.Label>
{t("tabs.settings")}
</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
</NativeTabs>
);
Expand Down
4 changes: 2 additions & 2 deletions src/app/(app)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Redirect } from "expo-router";
import { useAuth } from "@/shared/hooks/useAuth";

export default function Index() {
const { user, isAuthReady } = useAuth();
const { user, isAuthenticated } = useAuth();

if (isAuthReady) return null;
if (isAuthenticated) return null;

return <Redirect href={user ? "/(app)" : "/(auth)/signin"} />;
}
4 changes: 2 additions & 2 deletions src/app/(auth)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Redirect, Stack } from "expo-router";
import { useAuth } from "@/shared/hooks/useAuth";

export default function AuthLayout() {
const { isAuthReady, user } = useAuth();
if (isAuthReady) return null;
const { isAuthenticated, user } = useAuth();
if (isAuthenticated) return null;
if (user) return <Redirect href="/(app)" />;

return <Stack screenOptions={{ headerShown: true }} />;
Expand Down
6 changes: 3 additions & 3 deletions src/shared/auth/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import { AuthContext } from "./context";

export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
const [isAuthReady, setIsAuthReady] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);

useEffect(() => {
getSessionUser()
.then(setUser)
.finally(() => setIsAuthReady(false));
.finally(() => setIsAuthenticated(false));
}, []);

async function signIn(email: string, password: string): Promise<void> {
Expand All @@ -37,7 +37,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {

return (
<AuthContext.Provider
value={{ user, isAuthReady, signIn, signUp, signOut }}
value={{ user, isAuthenticated, signIn, signUp, signOut }}
>
{children}
</AuthContext.Provider>
Expand Down
51 changes: 51 additions & 0 deletions src/shared/auth/rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { z } from "zod";

export const MIN_PASSWORD = 10;
export const MAX_PASSWORD = 128;

const COMMON_PASSWORDS = new Set([
"12345678",
"123456789",
"1234567890",
"password",
"password123",
"qwerty123",
"admin123",
"letmein",
"11111111",
]);

export const gmailEmail = z
.string()
.trim()
.toLowerCase()
.email("Invalid email")
.refine((email) => email.endsWith("@gmail.com"), {
message: "Only @gmail.com emails are allowed",
})
.refine((email) => !email.includes("+"), {
message: "Gmail aliases using '+' are not allowed",
});

export const strongPassword = z
.string()
.min(MIN_PASSWORD, `Password must be at least ${MIN_PASSWORD} characters`)
.max(MAX_PASSWORD, `Password must be at most ${MAX_PASSWORD} characters`)
.refine((val) => !/\s/.test(val), {
message: "Password cannot contain spaces",
})
.refine((val) => /[a-z]/.test(val), {
message: "Password must include at least 1 lowercase letter",
})
.refine((val) => /[A-Z]/.test(val), {
message: "Password must include at least 1 uppercase letter",
})
.refine((val) => /\d/.test(val), {
message: "Password must include at least 1 number",
})
.refine((val) => /[^A-Za-z0-9]/.test(val), {
message: "Password must include at least 1 special character",
})
.refine((val) => !COMMON_PASSWORDS.has(val.toLowerCase()), {
message: "Password is too common",
});
15 changes: 15 additions & 0 deletions src/shared/auth/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { z } from "zod";
import { gmailEmail, strongPassword } from "./rules";

export const signInSchema = z.object({
email: gmailEmail,
password: z.string().min(1, "Password is required"),
});

export const signUpSchema = z.object({
email: gmailEmail,
password: strongPassword,
});

export type SignInSchema = z.infer<typeof signInSchema>;
export type SignUpSchema = z.infer<typeof signUpSchema>;
2 changes: 1 addition & 1 deletion src/shared/components/sign-in-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useRouter } from "expo-router/build/hooks";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";
import { Pressable, type TextInput, View } from "react-native";
import { type SignInSchema, signInSchema } from "@/shared/auth/schemas";
import { SocialConnections } from "@/shared/components/social-connections";
import { Button } from "@/shared/components/ui/button";
import {
Expand All @@ -17,7 +18,6 @@ import { Label } from "@/shared/components/ui/label";
import { Separator } from "@/shared/components/ui/separator";
import { Text } from "@/shared/components/ui/text";
import { useAuth } from "../hooks/useAuth";
import { type SignInSchema, signInSchema } from "../types/auth";

export function SignInForm() {
const router = useRouter();
Expand Down
2 changes: 1 addition & 1 deletion src/shared/components/sign-up-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useRouter } from "expo-router";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";
import { Pressable, type TextInput, View } from "react-native";
import { type SignUpSchema, signUpSchema } from "@/shared/auth/schemas";
import { SocialConnections } from "@/shared/components/social-connections";
import { Button } from "@/shared/components/ui/button";
import {
Expand All @@ -16,7 +17,6 @@ import { Input } from "@/shared/components/ui/input";
import { Label } from "@/shared/components/ui/label";
import { Separator } from "@/shared/components/ui/separator";
import { Text } from "@/shared/components/ui/text";
import { type SignUpSchema, signUpSchema } from "@/shared/types/auth";
import { useAuth } from "../hooks/useAuth";

export function SignUpForm() {
Expand Down
17 changes: 1 addition & 16 deletions src/shared/types/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { z } from "zod";

export type AuthContextType = {
user: AuthUser | null;
isAuthReady: boolean;
isAuthenticated: boolean;
signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
Expand All @@ -12,16 +10,3 @@ export type AuthUser = {
id: number;
email: string;
};

export const signInSchema = z.object({
email: z.email("Invalid email"),
password: z.string().min(6, "Password must be at least 6 characters"),
});

export const signUpSchema = z.object({
email: z.email("Invalid email"),
password: z.string().min(6, "Password must be at least 6 characters"),
});

export type SignInSchema = z.infer<typeof signInSchema>;
export type SignUpSchema = z.infer<typeof signUpSchema>;
Loading