📖 Overview
Di bagian ini, kita akan membangun sistem autentikasi lengkap:
- ✅ Login screen dengan form validation
- ✅ State management dengan Zustand
- ✅ Token storage dengan SecureStore
- ✅ Protected routes dengan auth guard
- ✅ Logout functionality
🔐 Step 1: Buat Auth Store (Zustand)
State management untuk user authentication.
File: store/auth.ts
import { create } from 'zustand';
import * as SecureStore from 'expo-secure-store';
interface User {
id: string;
name: string;
email: string;
role: string;
picture?: string;
}
interface AuthState {
token: string | null;
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
// Actions
setAuth: (token: string, user: User) => Promise<void>;
logout: () => Promise<void>;
initialize: () => Promise<void>;
}
export const useAuthStore = create<AuthState>((set, get) => ({
token: null,
user: null,
isLoading: true,
isAuthenticated: false,
setAuth: async (token: string, user: User) => {
try {
// Simpan ke SecureStore
await SecureStore.setItemAsync('auth_token', token);
await SecureStore.setItemAsync('auth_user', JSON.stringify(user));
set({
token,
user,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
console.error('Error saving auth:', error);
}
},
logout: async () => {
try {
// Hapus dari SecureStore
await SecureStore.deleteItemAsync('auth_token');
await SecureStore.deleteItemAsync('auth_user');
set({
token: null,
user: null,
isAuthenticated: false,
isLoading: false,
});
} catch (error) {
console.error('Error clearing auth:', error);
}
},
initialize: async () => {
try {
// Load dari SecureStore saat app start
const token = await SecureStore.getItemAsync('auth_token');
const userJson = await SecureStore.getItemAsync('auth_user');
if (token && userJson) {
const user = JSON.parse(userJson);
set({
token,
user,
isAuthenticated: true,
isLoading: false,
});
} else {
set({ isLoading: false });
}
} catch (error) {
console.error('Error initializing auth:', error);
set({ isLoading: false });
}
},
}));
📱 Step 2: Buat Login Screen
File: app/login.tsx
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
Image,
} from 'react-native';
import { router } from 'expo-router';
import { loginAdmin } from '@/services/auth';
import { useAuthStore } from '@/store/auth';
export default function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState<{email?: string; password?: string}>({});
const { setAuth } = useAuthStore();
// Validasi form
const validateForm = (): boolean => {
const newErrors: {email?: string; password?: string} = {};
if (!email) {
newErrors.email = 'Email wajib diisi';
} else if (!/\S+@\S+\.\S+/.test(email)) {
newErrors.email = 'Format email tidak valid';
}
if (!password) {
newErrors.password = 'Password wajib diisi';
} else if (password.length < 6) {
newErrors.password = 'Password minimal 6 karakter';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleLogin = async () => {
if (!validateForm()) return;
setIsLoading(true);
try {
const response = await loginAdmin(email, password);
if (response.success && response.data) {
await setAuth(response.data.token, response.data.user);
router.replace('/(tabs)');
} else {
Alert.alert('Login Gagal', response.error || 'Email atau password salah');
}
} catch (error: any) {
console.error('Login error:', error);
Alert.alert(
'Error',
error.response?.data?.error || 'Terjadi kesalahan. Coba lagi.'
);
} finally {
setIsLoading(false);
}
};
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
className="flex-1 bg-secondary"
>
<View className="flex-1 justify-center px-8">
{/* Logo */}
<View className="items-center mb-12">
<Image
source={require('@/assets/images/icon.png')}
className="w-24 h-24 mb-4"
resizeMode="contain"
/>
<Text className="text-3xl font-bold text-white">
Lampung Dev
</Text>
<Text className="text-gray-400 mt-2">
Admin Event Management
</Text>
</View>
{/* Form */}
<View className="space-y-4">
{/* Email Input */}
<View>
<Text className="text-gray-300 mb-2">Email</Text>
<TextInput
className={`bg-gray-800 text-white px-4 py-4 rounded-xl ${
errors.email ? 'border border-red-500' : ''
}`}
placeholder="[email protected]"
placeholderTextColor="#666"
value={email}
onChangeText={(text) => {
setEmail(text);
setErrors({...errors, email: undefined});
}}
keyboardType="email-address"
autoCapitalize="none"
autoComplete="email"
/>
{errors.email && (
<Text className="text-red-500 text-sm mt-1">
{errors.email}
</Text>
)}
</View>
{/* Password Input */}
<View className="mt-4">
<Text className="text-gray-300 mb-2">Password</Text>
<TextInput
className={`bg-gray-800 text-white px-4 py-4 rounded-xl ${
errors.password ? 'border border-red-500' : ''
}`}
placeholder="••••••••"
placeholderTextColor="#666"
value={password}
onChangeText={(text) => {
setPassword(text);
setErrors({...errors, password: undefined});
}}
secureTextEntry
autoComplete="password"
/>
{errors.password && (
<Text className="text-red-500 text-sm mt-1">
{errors.password}
</Text>
)}
</View>
{/* Login Button */}
<TouchableOpacity
className={`mt-8 py-4 rounded-xl items-center ${
isLoading ? 'bg-primary/50' : 'bg-primary'
}`}
onPress={handleLogin}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="#fff" />
) : (
<Text className="text-white font-bold text-lg">
Login
</Text>
)}
</TouchableOpacity>
</View>
{/* Footer */}
<Text className="text-gray-500 text-center mt-8 text-sm">
Hanya untuk admin/panitia event
</Text>
</View>
</KeyboardAvoidingView>
);
}
🛡️ Step 3: Setup Auth Guard di Root Layout
Edit file app/_layout.tsx untuk menambahkan auth guard:
File: app/_layout.tsx
import { useEffect } from 'react';
import { Stack, router, useSegments, useRootNavigationState } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { View, ActivityIndicator } from 'react-native';
import { useAuthStore } from '@/store/auth';
import '../global.css';
function AuthGuard({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading, initialize } = useAuthStore();
const segments = useSegments();
const navigationState = useRootNavigationState();
useEffect(() => {
// Initialize auth state saat app start
initialize();
}, []);
useEffect(() => {
if (!navigationState?.key || isLoading) return;
const inAuthGroup = segments[0] === 'login';
if (!isAuthenticated && !inAuthGroup) {
// User belum login, redirect ke login
router.replace('/login');
} else if (isAuthenticated && inAuthGroup) {
// User sudah login, redirect ke home
router.replace('/(tabs)');
}
}, [isAuthenticated, segments, isLoading, navigationState?.key]);
// Loading screen
if (isLoading) {
return (
<View className="flex-1 items-center justify-center bg-secondary">
<ActivityIndicator size="large" color="#f0880a" />
</View>
);
}
return <>{children}</>;
}
export default function RootLayout() {
return (
<AuthGuard>
<StatusBar style="light" />
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: '#1a1a1a' },
}}
>
<Stack.Screen name="login" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="event/[id]" options={{ headerShown: true }} />
</Stack>
</AuthGuard>
);
}
🔄 Step 4: Update Tab Layout dengan User Info
Edit file app/(tabs)/_layout.tsx:
File: app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Home, Scan, User } from 'lucide-react-native';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from 'react-native';
export default function TabLayout() {
const colorScheme = useColorScheme() ?? 'dark';
const colors = Colors[colorScheme];
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: colors.tint,
tabBarInactiveTintColor: colors.tabIconDefault,
tabBarStyle: {
backgroundColor: colors.background,
borderTopColor: colors.border,
paddingBottom: 5,
paddingTop: 5,
height: 60,
},
headerStyle: {
backgroundColor: colors.background,
},
headerTintColor: colors.text,
headerShadowVisible: false,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Events',
tabBarIcon: ({ color, size }) => (
<Home size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="scanner"
options={{
title: 'Scanner',
tabBarIcon: ({ color, size }) => (
<Scan size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="account"
options={{
title: 'Akun',
tabBarIcon: ({ color, size }) => (
<User size={size} color={color} />
),
}}
/>
</Tabs>
);
}
👤 Step 5: Buat Account Screen dengan Logout
File: app/(tabs)/account.tsx
import React from 'react';
import { View, Text, TouchableOpacity, Alert, Image } from 'react-native';
import { router } from 'expo-router';
import { LogOut, Mail, Shield } from 'lucide-react-native';
import { useAuthStore } from '@/store/auth';
export default function AccountScreen() {
const { user, logout } = useAuthStore();
const handleLogout = () => {
Alert.alert(
'Konfirmasi Logout',
'Apakah kamu yakin ingin keluar?',
[
{ text: 'Batal', style: 'cancel' },
{
text: 'Logout',
style: 'destructive',
onPress: async () => {
await logout();
router.replace('/login');
}
},
]
);
};
return (
<View className="flex-1 bg-secondary">
{/* Profile Header */}
<View className="items-center pt-8 pb-6 bg-gray-900">
{user?.picture ? (
<Image
source={{ uri: user.picture }}
className="w-24 h-24 rounded-full"
/>
) : (
<View className="w-24 h-24 rounded-full bg-primary items-center justify-center">
<Text className="text-white text-3xl font-bold">
{user?.name?.charAt(0).toUpperCase()}
</Text>
</View>
)}
<Text className="text-white text-xl font-bold mt-4">
{user?.name}
</Text>
</View>
{/* Info Cards */}
<View className="px-4 pt-6 space-y-3">
{/* Email */}
<View className="flex-row items-center bg-gray-800 p-4 rounded-xl">
<Mail size={20} color="#f0880a" />
<View className="ml-3">
<Text className="text-gray-400 text-sm">Email</Text>
<Text className="text-white">{user?.email}</Text>
</View>
</View>
{/* Role */}
<View className="flex-row items-center bg-gray-800 p-4 rounded-xl mt-3">
<Shield size={20} color="#f0880a" />
<View className="ml-3">
<Text className="text-gray-400 text-sm">Role</Text>
<Text className="text-white capitalize">{user?.role}</Text>
</View>
</View>
</View>
{/* Logout Button */}
<View className="px-4 mt-8">
<TouchableOpacity
className="flex-row items-center justify-center bg-red-600 p-4 rounded-xl"
onPress={handleLogout}
>
<LogOut size={20} color="#fff" />
<Text className="text-white font-bold ml-2">
Logout
</Text>
</TouchableOpacity>
</View>
{/* App Version */}
<View className="absolute bottom-8 w-full items-center">
<Text className="text-gray-500 text-sm">
Lampung Dev Mobile v1.0.0
</Text>
</View>
</View>
);
}
🔄 Rangkuman Authentication Flow
Inisialisasi Aplikasi
| Step | Aksi | Hasil |
|---|---|---|
| 1️⃣ | App mulai | Jalankan initialize() |
| 2️⃣ | Load dari SecureStore | Cek token & user tersimpan |
| 3️⃣ | Token ditemukan? | Ya → Ke Home (Tabs) |
| 4️⃣ | Tidak ada token? | Tidak → Ke Login Screen |
Flow Login
| Step | Aksi | Hasil |
|---|---|---|
| 1️⃣ | User isi kredensial | Input email & password |
| 2️⃣ | Validasi form | Cek field wajib & format |
| 3️⃣ | Panggil loginAdmin() | API POST ke /api/admin/auth |
| 4️⃣ | API validasi | Server cek kredensial |
| 5️⃣ | Terima response | { token, user } jika sukses |
| 6️⃣ | Panggil setAuth() | Simpan ke SecureStore (terenkripsi) |
| 7️⃣ | Navigate ke Home | Redirect ke /(tabs) |
Flow Logout
| Step | Aksi | Hasil |
|---|---|---|
| 1️⃣ | User klik logout | Jalankan logout() |
| 2️⃣ | Hapus SecureStore | Delete token & user data |
| 3️⃣ | Reset state | Set token & user ke null |
| 4️⃣ | Auth guard deteksi | Token tidak ditemukan |
| 5️⃣ | Redirect ke Login | Navigate ke /login |
🧪 Testing Authentication
Test 1: Login dengan Kredensial Valid
- Jalankan app dengan
npm start - Buka Expo Go di HP
- App akan redirect ke Login screen
- Masukkan email dan password admin
- Setelah sukses, akan redirect ke Tab screen
Test 2: Persistent Login
- Login seperti biasa
- Tutup app (kill process)
- Buka app lagi
- Harusnya langsung masuk ke Tab screen (tidak perlu login lagi)
Test 3: Logout
- Dari Tab screen, pergi ke tab "Akun"
- Tap tombol "Logout"
- Konfirmasi logout
- App akan redirect ke Login screen
✅ Checklist
- Auth store dengan Zustand dibuat
- Login screen dengan form validation dibuat
- Auth guard di root layout berfungsi
- Tab layout dengan icons dibuat
- Account screen dengan logout dibuat
- Persistent login berfungsi
🚀 Selanjutnya
Di Part 5, kita akan membuat fitur manajemen event termasuk list event, event card component, detail screen, dan participant list.
← Part 3: Project Setup | Part 5: Manajemen Event →
Series ini dibuat untuk mendukung materi Casual Meetup #15 di LampungDev.
