React Native Expo Event Management

1

Part 1: Pengenalan & Arsitektur

2

Part 2: Instalasi di Mac, Windows, Ubuntu

3

Part 3: Project Setup

4

Part 4: Autentikasi Admin

5

Part 5: Manajemen Event

6

Part 6: QR Scanner Kehadiran

7

Part 7: Polish & Deploy

Artikel ini tersedia dalam Bahasa Inggris

🇬🇧 Read in English

21 Januari 2026

🇮🇩 Bahasa Indonesia

React Native Expo Event Management Part 4: Autentikasi Admin

Implementasi sistem login admin dengan form validation, state management Zustand, token storage SecureStore, dan protected routes.

9 min read


📖 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

StepAksiHasil
1️⃣App mulaiJalankan initialize()
2️⃣Load dari SecureStoreCek token & user tersimpan
3️⃣Token ditemukan?Ya → Ke Home (Tabs)
4️⃣Tidak ada token?Tidak → Ke Login Screen

Flow Login

StepAksiHasil
1️⃣User isi kredensialInput email & password
2️⃣Validasi formCek field wajib & format
3️⃣Panggil loginAdmin()API POST ke /api/admin/auth
4️⃣API validasiServer cek kredensial
5️⃣Terima response{ token, user } jika sukses
6️⃣Panggil setAuth()Simpan ke SecureStore (terenkripsi)
7️⃣Navigate ke HomeRedirect ke /(tabs)

Flow Logout

StepAksiHasil
1️⃣User klik logoutJalankan logout()
2️⃣Hapus SecureStoreDelete token & user data
3️⃣Reset stateSet token & user ke null
4️⃣Auth guard deteksiToken tidak ditemukan
5️⃣Redirect ke LoginNavigate ke /login

🧪 Testing Authentication

Test 1: Login dengan Kredensial Valid

  1. Jalankan app dengan npm start
  2. Buka Expo Go di HP
  3. App akan redirect ke Login screen
  4. Masukkan email dan password admin
  5. Setelah sukses, akan redirect ke Tab screen

Test 2: Persistent Login

  1. Login seperti biasa
  2. Tutup app (kill process)
  3. Buka app lagi
  4. Harusnya langsung masuk ke Tab screen (tidak perlu login lagi)

Test 3: Logout

  1. Dari Tab screen, pergi ke tab "Akun"
  2. Tap tombol "Logout"
  3. Konfirmasi logout
  4. 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.

Lanjut Membaca

Previous article thumbnail

← Artikel Sebelumnya

React Native Expo Event Management Part 3: Project Setup

Artikel Selanjutnya →

React Native Expo Event Management Part 5: Manajemen Event

Next article thumbnail