React Native Expo Event Management

1

Part 1: Introduction & Architecture

2

Part 2: Installation on Mac, Windows, Ubuntu

3

Part 3: Project Setup

4

Part 4: Admin Authentication

5

Part 5: Event Management

6

Part 6: QR Scanner for Attendance

7

Part 7: Polish & Deploy

This article is available in Indonesian

šŸ‡®šŸ‡© Baca dalam Bahasa Indonesia

January 21, 2026

šŸ‡¬šŸ‡§ English

React Native Expo Event Management Part 4: Admin Authentication

Implement secure authentication with Zustand state management, SecureStore for token storage, login screen, and auth guard for protected routes.

6 min read


šŸ“– Overview

In this part, we'll implement:

  • Zustand store for authentication state
  • Login screen with form validation
  • Auth guard for protected routes
  • Secure token storage with SecureStore

šŸ—ƒļø Step 1: Create Auth Store

Zustand is a lightweight state management library that's perfect for React Native.

File: store/auth.ts

import { create } from 'zustand';
import * as SecureStore from 'expo-secure-store';

// Types
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>;
}

// Store
export const useAuthStore = create<AuthState>((set, get) => ({
  token: null,
  user: null,
  isLoading: true,
  
  // Computed property
  get isAuthenticated() {
    return !!get().token;
  },

  // Set authentication data
  setAuth: async (token, user) => {
    try {
      await SecureStore.setItemAsync('auth_token', token);
      await SecureStore.setItemAsync('auth_user', JSON.stringify(user));
      set({ token, user, isLoading: false });
    } catch (error) {
      console.error('Error saving auth:', error);
      throw error;
    }
  },

  // Logout - clear everything
  logout: async () => {
    try {
      await SecureStore.deleteItemAsync('auth_token');
      await SecureStore.deleteItemAsync('auth_user');
      set({ token: null, user: null, isLoading: false });
    } catch (error) {
      console.error('Error during logout:', error);
      set({ token: null, user: null, isLoading: false });
    }
  },

  // Initialize - load from storage
  initialize: async () => {
    try {
      const token = await SecureStore.getItemAsync('auth_token');
      const userStr = await SecureStore.getItemAsync('auth_user');

      if (token && userStr) {
        const user = JSON.parse(userStr);
        set({ token, user, isLoading: false });
      } else {
        set({ isLoading: false });
      }
    } catch (error) {
      console.error('Failed to initialize auth:', error);
      set({ isLoading: false });
    }
  },
}));

File: store/index.ts

export { useAuthStore } from './auth';

šŸ“± Step 2: Create Login Screen

File: app/login.tsx

import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  KeyboardAvoidingView,
  Platform,
  ActivityIndicator,
  Alert,
  Image,
} from 'react-native';
import { useRouter } from 'expo-router';
import { Mail, Lock, LogIn } from 'lucide-react-native';
import { loginAdmin } from '@/services/auth';
import { useAuthStore } from '@/store/auth';

export default function LoginScreen() {
  const router = useRouter();
  const setAuth = useAuthStore((state) => state.setAuth);
  
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [errors, setErrors] = useState<{ email?: string; password?: string }>({});

  // Validate form
  const validate = (): boolean => {
    const newErrors: { email?: string; password?: string } = {};
    
    if (!email.trim()) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(email)) {
      newErrors.email = 'Invalid email format';
    }
    
    if (!password) {
      newErrors.password = 'Password is required';
    } else if (password.length < 6) {
      newErrors.password = 'Password must be at least 6 characters';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  // Handle login
  const handleLogin = async () => {
    if (!validate()) 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 Failed', response.error || 'Invalid credentials');
      }
    } catch (error: any) {
      console.error('Login error:', error);
      Alert.alert('Error', 'An error occurred. Please try again.');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <KeyboardAvoidingView
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
      className="flex-1 bg-gray-900"
    >
      <View className="flex-1 justify-center px-6">
        {/* Logo */}
        <View className="items-center mb-10">
          <Image
            source={require('@/assets/images/icon.png')}
            className="w-24 h-24 rounded-2xl"
          />
          <Text className="text-3xl font-bold text-white mt-4">
            Lampung Dev
          </Text>
          <Text className="text-gray-400 mt-2">
            Admin Portal
          </Text>
        </View>

        {/* Form */}
        <View className="space-y-4">
          {/* Email Input */}
          <View>
            <View className="flex-row items-center bg-gray-800 rounded-xl px-4 py-3">
              <Mail size={20} color="#9ca3af" />
              <TextInput
                className="flex-1 ml-3 text-white text-base"
                placeholder="Email"
                placeholderTextColor="#6b7280"
                value={email}
                onChangeText={setEmail}
                keyboardType="email-address"
                autoCapitalize="none"
              />
            </View>
            {errors.email && (
              <Text className="text-red-500 text-sm mt-1 ml-1">
                {errors.email}
              </Text>
            )}
          </View>

          {/* Password Input */}
          <View className="mt-4">
            <View className="flex-row items-center bg-gray-800 rounded-xl px-4 py-3">
              <Lock size={20} color="#9ca3af" />
              <TextInput
                className="flex-1 ml-3 text-white text-base"
                placeholder="Password"
                placeholderTextColor="#6b7280"
                value={password}
                onChangeText={setPassword}
                secureTextEntry
              />
            </View>
            {errors.password && (
              <Text className="text-red-500 text-sm mt-1 ml-1">
                {errors.password}
              </Text>
            )}
          </View>

          {/* Login Button */}
          <TouchableOpacity
            className={`flex-row items-center justify-center py-4 rounded-xl mt-6 ${
              isLoading ? 'bg-orange-400' : 'bg-orange-500'
            }`}
            onPress={handleLogin}
            disabled={isLoading}
          >
            {isLoading ? (
              <ActivityIndicator color="white" />
            ) : (
              <>
                <LogIn size={20} color="white" />
                <Text className="text-white font-semibold text-lg ml-2">
                  Login
                </Text>
              </>
            )}
          </TouchableOpacity>
        </View>
      </View>
    </KeyboardAvoidingView>
  );
}

šŸ›”ļø Step 3: Setup Auth Guard

File: app/_layout.tsx

import React, { useEffect } from 'react';
import { Stack, useRouter, useSegments } 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 router = useRouter();
  const segments = useSegments();
  const { token, isLoading, initialize } = useAuthStore();

  useEffect(() => {
    initialize();
  }, []);

  useEffect(() => {
    if (isLoading) return;

    const inAuthGroup = segments[0] === 'login';
    const isAuthenticated = !!token;

    if (!isAuthenticated && !inAuthGroup) {
      router.replace('/login');
    } else if (isAuthenticated && inAuthGroup) {
      router.replace('/(tabs)');
    }
  }, [token, isLoading, segments]);

  if (isLoading) {
    return (
      <View className="flex-1 bg-gray-900 items-center justify-center">
        <ActivityIndicator size="large" color="#f0880a" />
      </View>
    );
  }

  return <>{children}</>;
}

export default function RootLayout() {
  return (
    <AuthGuard>
      <StatusBar style="light" />
      <Stack
        screenOptions={{
          headerShown: false,
          contentStyle: { backgroundColor: '#111827' },
          animation: 'slide_from_right',
        }}
      >
        <Stack.Screen name="login" />
        <Stack.Screen name="(tabs)" />
        <Stack.Screen 
          name="event/[id]" 
          options={{
            headerShown: true,
            headerTitle: 'Event Detail',
            headerStyle: { backgroundColor: '#111827' },
            headerTintColor: '#fff',
          }}
        />
      </Stack>
    </AuthGuard>
  );
}

šŸ”„ Auth Flow Diagram

App Initialization

StepActionResult
1ļøāƒ£App startsTrigger initialize()
2ļøāƒ£Load from SecureStoreCheck for saved token & user
3ļøāƒ£Token found?Yes → Go to Home (Tabs)
4ļøāƒ£No token?No → Go to Login Screen

Login Flow

StepActionResult
1ļøāƒ£User enters credentialsEmail & password input
2ļøāƒ£Form validationCheck required fields & format
3ļøāƒ£Call loginAdmin()API POST to /api/admin/auth
4ļøāƒ£API validatesServer checks credentials
5ļøāƒ£Receive response{ token, user } on success
6ļøāƒ£Call setAuth()Save to SecureStore (encrypted)
7ļøāƒ£Navigate to HomeRedirect to /(tabs)

Logout Flow

StepActionResult
1ļøāƒ£User clicks logoutTrigger logout()
2ļøāƒ£Clear SecureStoreDelete token & user data
3ļøāƒ£Reset stateSet token & user to null
4ļøāƒ£Auth guard detectsNo token found
5ļøāƒ£Redirect to LoginNavigate to /login

šŸ” Security Best Practices

āœ… DOāŒ DON'T
Use SecureStore for tokensStore tokens in AsyncStorage
Validate input before APILog sensitive data
Clear tokens on logoutStore passwords locally
Handle 401 globallyTrust client-side only
Use HTTPS in productionSkip server validation

āœ… Checklist

  • Auth store created with Zustand
  • Login screen with form validation
  • Auth guard in root layout
  • SecureStore for token storage
  • Auto-redirect based on auth state

šŸš€ Next Steps

In Part 5, we'll implement event management with event list, event detail, and participant list.


← Part 3: Project Setup | Next: Event Management →


This series is created to support the Casual Meetup #15 at LampungDev.

Continue Reading

Previous article

← Previous Article

React Native Expo Event Management Part 3: Project Setup

Next Article →

React Native Expo Event Management Part 5: Manajemen Event

Next article