Skip to main content
The Incidents App uses QR code scanning to provide secure, passwordless authentication for hotel guests.

Overview

Guests receive a QR code at check-in that grants them access to the incident reporting system. The QR code contains an access code linked to their room and reservation.
QR code authentication eliminates the need for guests to remember passwords while maintaining security through time-limited access codes.

How It Works

1

QR Code Generation

The hotel generates a QR code for each guest containing a unique access code linked to their room reservation.
2

Guest Scans Code

Guests scan the QR code using the app’s camera to authenticate.
3

Session Validation

The app validates the access code against the database and checks expiration.
4

Session Creation

A secure guest session is created and stored locally on the device.

Implementation

The QR scanning functionality is implemented in the scan.tsx screen using Expo Camera:
scan.tsx
import { CameraView, useCameraPermissions } from 'expo-camera'
import * as SecureStore from 'expo-secure-store'

export default function GuestScanScreen() {
  const [permission, requestPermission] = useCameraPermissions()
  const [scanning, setScanning] = useState(false)
  const scanLock = useRef(false)

  async function startScan() {
    if (!permission || permission.status !== 'granted') {
      const res = await requestPermission()
      if (!res.granted) {
        Alert.alert(
          'Permiso requerido',
          'Necesitamos acceso a la cámara para escanear el código QR'
        )
        return
      }
    }

    scanLock.current = false
    setScanning(true)
  }
}

Camera Permissions

The app requests camera permissions before scanning:
  1. Check if permission is already granted
  2. Request permission if needed
  3. Display alert if permission is denied
  4. Enable scanning once permission is granted
const [permission, requestPermission] = useCameraPermissions()

if (!permission || permission.status !== 'granted') {
  const res = await requestPermission()
  if (!res.granted) {
    Alert.alert(
      'Permiso requerido',
      'Necesitamos acceso a la cámara para escanear el código QR'
    )
    return
  }
}

Scanning Process

The camera view is configured to detect QR codes and handle the scan result:
<CameraView
  style={StyleSheet.absoluteFillObject}
  barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
  onBarcodeScanned={handleScan}
/>

Scan Lock Mechanism

To prevent multiple scans, a ref-based lock is used:
const scanLock = useRef(false)

async function handleScan({ data }: { data: string }) {
  if (scanLock.current) return
  scanLock.current = true

  setScanning(false)
  // Process scan...
}
The scan lock prevents duplicate processing if the camera detects the same QR code multiple times.

Data Validation

The scanned data is parsed and validated:
let accessCode
try {
  const parsed = JSON.parse(data)
  accessCode = parsed.access_code
} catch {
  accessCode = data
}

if (!accessCode) {
  Alert.alert('Código inválido', 'El QR no contiene un código válido', [
    {
      text: 'Intentar de nuevo',
      onPress: () => {
        scanLock.current = false
      },
    },
  ])
  return
}
The code supports both JSON format ({"access_code": "..."}}) and plain text format for flexibility.

Session Verification

The access code is verified against the database:
const { data: session, error } = await supabase
  .from('guest_sessions')
  .select('*')
  .eq('access_code', accessCode)
  .eq('active', true)
  .single()

if (error || !session) {
  Alert.alert(
    'Código inválido',
    'El código no es válido o ya expiró',
    [
      {
        text: 'Intentar de nuevo',
        onPress: () => {
          scanLock.current = false
        },
      },
    ]
  )
  return
}

Validation Checks

Access Code Match

Verifies the code exists in the database

Active Status

Ensures the session is currently active

Expiration Check

Validates the session hasn’t expired

Room Link

Confirms the session is linked to a valid room

Expiration Handling

The app checks if the session has expired:
const now = new Date()
if (new Date(session.expires_at) < now) {
  Alert.alert(
    'Código expirado',
    'Este código ya no es válido',
    [
      {
        text: 'Aceptar',
        onPress: () => {
          scanLock.current = false
        },
      },
    ]
  )
  return
}
Expired sessions require guests to obtain a new QR code from hotel staff.

Session Storage

Successful scans store the session data securely:
await SecureStore.setItemAsync('guest_session', JSON.stringify(session))
router.replace('/(guest)/home')
The stored session contains:
  • access_code: The unique access code
  • room_id: Link to the guest’s room
  • expires_at: Session expiration timestamp
  • active: Whether the session is active

Camera UI

The scanning interface includes a visual frame and helper text:
<View style={styles.overlay}>
  <View style={styles.scanFrame} />
  <AppText style={styles.helperText}>
    Alinea el código QR dentro del marco
  </AppText>

  <TouchableOpacity
    style={styles.cancelButton}
    onPress={() => {
      scanLock.current = false
      setScanning(false)
    }}
  >
    <AppText style={styles.cancelText}>Cancelar</AppText>
  </TouchableOpacity>
</View>
styles
const styles = StyleSheet.create({
  scanFrame: {
    width: 240,
    height: 240,
    borderWidth: 2,
    borderColor: '#FF385C',
    borderRadius: 16,
  },
  overlay: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
})

Error Handling

The scanning process handles multiple error scenarios:
Displays an alert explaining why camera access is needed.
Shows error message and allows retry by unlocking the scan.
Informs the guest the code has expired and needs replacement.
Validates the session is still active and hasn’t been deactivated.

Database Schema

The guest_sessions table structure:
CREATE TABLE guest_sessions (
  id UUID PRIMARY KEY,
  access_code TEXT UNIQUE NOT NULL,
  room_id UUID REFERENCES rooms(id),
  active BOOLEAN DEFAULT true,
  expires_at TIMESTAMP NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_guest_sessions_access_code ON guest_sessions(access_code);
CREATE INDEX idx_guest_sessions_active ON guest_sessions(active);

Security Features

Secure Storage

Sessions are stored using Expo SecureStore with encryption

Time-Limited Access

Sessions expire automatically to prevent unauthorized access

One-Time Use

Access codes can be deactivated after check-out

Room Isolation

Each session is linked to a specific room

Best Practices

  1. Always validate permissions before attempting to scan
  2. Use scan locks to prevent duplicate processing
  3. Handle all error cases with user-friendly messages
  4. Store sessions securely using SecureStore
  5. Validate expiration on both client and server

Guest Sessions

Learn about session management

Security

Understand security measures

Build docs developers (and LLMs) love