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 6: QR Scanner for Attendance

Implement QR code scanner using expo-camera for attendance check-in with camera permissions, QR data parsing, and animated success/error feedback.

5 min read


šŸ“– Overview

In this part, we'll implement:

  • Camera permission handling
  • QR code scanning with expo-camera
  • QR data parsing (format: eventId:uniqueCode)
  • Attendance recording via API
  • Animated success/error feedback

šŸŽÆ QR Code Format

The QR code contains participant registration data:

eventId:uniqueCode Example: abc123:xyz789

šŸ“¦ Step 1: Create Attendance Service

File: services/attendance.ts

import { api } from './api';

interface AttendanceResponse {
  success: boolean;
  data?: {
    id: string;
    attendee: {
      name: string;
      email: string;
    };
    event: {
      title: string;
    };
    checkedInAt: string;
  };
  error?: string;
}

export const recordAttendance = async (
  eventId: string,
  uniqueCode: string
): Promise<AttendanceResponse> => {
  try {
    const response = await api.post('/attendance', {
      eventId,
      uniqueCode,
    });
    return response.data;
  } catch (error: any) {
    return {
      success: false,
      error: error.response?.data?.error || 'Failed to record attendance',
    };
  }
};

šŸ“± Step 2: Create Scanner Screen

File: app/(tabs)/scanner.tsx

import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Alert, Animated } from 'react-native';
import { CameraView, useCameraPermissions, BarcodeScanningResult } from 'expo-camera';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Camera, CheckCircle, XCircle, RefreshCw } from 'lucide-react-native';
import { recordAttendance } from '@/services/attendance';

type ScanResult = {
  type: 'success' | 'error';
  title: string;
  message: string;
} | null;

export default function ScannerScreen() {
  const [permission, requestPermission] = useCameraPermissions();
  const [isScanning, setIsScanning] = useState(true);
  const [isProcessing, setIsProcessing] = useState(false);
  const [result, setResult] = useState<ScanResult>(null);
  const [fadeAnim] = useState(new Animated.Value(0));

  // Handle barcode scan
  const handleBarcodeScanned = async ({ data }: BarcodeScanningResult) => {
    if (!isScanning || isProcessing) return;
    
    setIsProcessing(true);
    setIsScanning(false);

    try {
      // Parse QR data: format is "eventId:uniqueCode"
      const [eventId, uniqueCode] = data.split(':');
      
      if (!eventId || !uniqueCode) {
        showResult('error', 'Invalid QR Code', 'QR code format is invalid');
        return;
      }

      // Record attendance via API
      const response = await recordAttendance(eventId, uniqueCode);
      
      if (response.success && response.data) {
        showResult(
          'success',
          'Attendance Recorded!',
          `${response.data.attendee.name} checked in to ${response.data.event.title}`
        );
      } else {
        showResult('error', 'Failed', response.error || 'Could not record attendance');
      }
    } catch (error) {
      console.error('Scan error:', error);
      showResult('error', 'Error', 'An error occurred while processing');
    }
  };

  // Show result with animation
  const showResult = (type: 'success' | 'error', title: string, message: string) => {
    setResult({ type, title, message });
    setIsProcessing(false);
    
    Animated.sequence([
      Animated.timing(fadeAnim, {
        toValue: 1,
        duration: 300,
        useNativeDriver: true,
      }),
      Animated.delay(2500),
      Animated.timing(fadeAnim, {
        toValue: 0,
        duration: 300,
        useNativeDriver: true,
      }),
    ]).start(() => {
      setResult(null);
      setIsScanning(true);
    });
  };

  // Reset scanner
  const resetScanner = () => {
    setResult(null);
    setIsScanning(true);
    setIsProcessing(false);
  };

  // Permission not determined
  if (!permission) {
    return (
      <View className="flex-1 bg-gray-900 items-center justify-center">
        <Text className="text-white">Requesting camera permission...</Text>
      </View>
    );
  }

  // Permission denied
  if (!permission.granted) {
    return (
      <SafeAreaView className="flex-1 bg-gray-900 items-center justify-center p-6">
        <Camera size={64} color="#6b7280" />
        <Text className="text-white text-xl font-bold mt-6 text-center">
          Camera Access Required
        </Text>
        <Text className="text-gray-400 text-center mt-2 mb-6">
          We need camera access to scan QR codes for attendance
        </Text>
        <TouchableOpacity
          className="bg-orange-500 px-8 py-4 rounded-xl"
          onPress={requestPermission}
        >
          <Text className="text-white font-semibold">Grant Permission</Text>
        </TouchableOpacity>
      </SafeAreaView>
    );
  }

  return (
    <View className="flex-1 bg-gray-900">
      {/* Camera View */}
      <CameraView
        style={StyleSheet.absoluteFillObject}
        barcodeScannerSettings={{
          barcodeTypes: ['qr'],
        }}
        onBarcodeScanned={isScanning ? handleBarcodeScanned : undefined}
      />

      {/* Scan Frame Overlay */}
      <View className="flex-1 items-center justify-center">
        <View className="w-72 h-72 border-4 border-white rounded-3xl opacity-50" />
        <Text className="text-white mt-4 text-lg">
          {isScanning ? 'Point camera at QR code' : 'Processing...'}
        </Text>
      </View>

      {/* Result Overlay */}
      {result && (
        <Animated.View
          style={{ opacity: fadeAnim }}
          className={`absolute inset-0 items-center justify-center ${
            result.type === 'success' ? 'bg-green-900/90' : 'bg-red-900/90'
          }`}
        >
          {result.type === 'success' ? (
            <CheckCircle size={80} color="#22c55e" />
          ) : (
            <XCircle size={80} color="#ef4444" />
          )}
          <Text className="text-white text-2xl font-bold mt-6">
            {result.title}
          </Text>
          <Text className="text-white/80 text-center mt-2 px-8">
            {result.message}
          </Text>
        </Animated.View>
      )}

      {/* Reset Button (when not scanning) */}
      {!isScanning && !result && (
        <View className="absolute bottom-10 inset-x-0 items-center">
          <TouchableOpacity
            className="bg-orange-500 px-8 py-4 rounded-xl flex-row items-center"
            onPress={resetScanner}
          >
            <RefreshCw size={20} color="white" />
            <Text className="text-white font-semibold ml-2">Scan Again</Text>
          </TouchableOpacity>
        </View>
      )}
    </View>
  );
}

šŸ”„ Scanner Flow

Permission Flow

StepConditionAction
1ļøāƒ£User opens scannerCheck camera permission
2ļøāƒ£Permission grantedShow camera view
3ļøāƒ£Permission deniedShow "Request Permission" button

Scanning Flow

StepActionResult
1ļøāƒ£Point camera at QRDetect QR code
2ļøāƒ£Parse QR dataExtract eventId:uniqueCode
3ļøāƒ£Validate formatCheck if both values exist
4ļøāƒ£Call recordAttendance()API POST to /api/attendance
5ļøāƒ£Handle responseSuccess or Error

Result Feedback

ResultAnimationDurationNext Action
āœ… SuccessGreen overlay + CheckCircle icon2.5 secondsAuto-reset, ready to scan
āŒ ErrorRed overlay + XCircle icon2.5 secondsAuto-reset, ready to scan

šŸŽØ Step 3: Add Haptic Feedback (Optional)

For better user experience, add haptic feedback:

npx expo install expo-haptics
import * as Haptics from 'expo-haptics';

// In handleBarcodeScanned:
if (response.success) {
  Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} else {
  Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}

āœ… Checklist

  • Attendance service created
  • Camera permission handling
  • QR code scanning with expo-camera
  • QR data parsing
  • API integration
  • Success/error animations
  • Auto-reset functionality

šŸš€ Next Steps

In Part 7, we'll polish the app with splash screen, error boundaries, network status, and deploy to Google Play Store.


← Part 5: Event Management | Next: Polish & Deploy →


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

Continue Reading

Previous article

← Previous Article

React Native Expo Event Management Part 5: Manajemen Event

Next Article →

React Native Expo Event Management Part 7: Polish & Deploy

Next article