Skip to main content
Table mode activates automatically when a customer is within the restaurant’s geofence radius (50 meters by default). Customers scan a QR code at their table to start an ordering session with a menu tailored to their table type.

How it works

1

GPS geofencing detection

The app uses the Haversine formula to calculate the distance between the user’s GPS coordinates and the restaurant location:
src/components/location/ContextSwitcher.tsx
function calculateDistance(c1: Coordinates, c2: Coordinates): number {
  const R = 6_371_000;
  const φ1 = (c1.latitude * Math.PI) / 180;
  const φ2 = (c2.latitude * Math.PI) / 180;
  const Δφ = ((c2.latitude - c1.latitude) * Math.PI) / 180;
  const Δλ = ((c2.longitude - c1.longitude) * Math.PI) / 180;
  const a =
    Math.sin(Δφ / 2) ** 2 +
    Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
If the distance is ≤ 50 meters, the app switches to SCANNER mode.
2

QR code scanning

The camera activates and displays a branded overlay with an animated scan frame. See QR Scanner for details.
3

Table session initialization

When a valid QR code is scanned, the app:
  • Validates the table ID against the TABLES_DATA registry (src/lib/core/mockData.ts:9)
  • Stores the active table in useTableStore (src/stores/useTableStore.ts:10)
  • Triggers multimodal feedback (haptic + sound)
  • Navigates to the menu screen
4

Contextual menu display

The menu filters products based on the table’s menuType. See Contextual Menus for details.

Table types

Tables are configured with a menuType property that determines which products are visible:
Tables with menuType: 'FULL' display all product categories:
  • Food
  • Snacks
  • Drinks
  • Desserts
{
  id: 'TABLE_HALL_05',
  displayName: 'Salon 05',
  menuType: 'FULL',
  status: 'FREE',
  specialEvent: 'NONE'
}

State management

Table mode uses three Zustand stores:

Location store

src/stores/useLocationStore.ts
interface LocationState {
  appMode: AppMode;              // 'CHECKING' | 'SCANNER' | 'DELIVERY'
  userLocation: Coordinates | null;
  restaurantLocation: Coordinates | null;
  setAppMode: (mode: AppMode) => void;
}
When geofencing detects the user is within range (src/components/location/ContextSwitcher.tsx:84):
if (distanceMeters <= Config.restaurant.geofenceRadiusMeters) {
  setAppMode('SCANNER');
  setServiceType('TABLE');
}

Table store

Manages the active table session:
const setTable = useTableStore((s) => s.setTable);
setTable(data); // stores TableData object

Cart store

Handles orders and birthday discounts:
src/stores/useCartStore.ts
setBirthdayMode: (active: boolean, discount = 0) => {
  const { items } = get();
  set({
    isBirthdayMode: active,
    discount: active ? discount : 0,
    total: calcTotal(items, active ? discount : 0),
  });
}
The birthday discount is applied immediately when the table is scanned (src/lib/modules/menu/useMenuLogic.ts:22), not at checkout.

QR code format

Table QR codes must match the format TABLE_* to be recognized. Test codes:
QR CodeTableMenu TypeSpecial Event
TABLE_BAR_01Barra 01Drinks onlyNone
TABLE_HALL_05Salon 05Full menuNone
TABLE_BDAY_99Mesa EspecialFull menuBirthday (15% off)
QR codes that don’t exist in TABLES_DATA trigger an error toast and reset the scanner to idle state (src/components/scanner/CameraScanner.tsx:60).

Session lifecycle

The session is cleared after successful payment (src/app/(checkout)/payment.tsx:252):
clearSession();  // resets useTableStore
resetLocation(); // resets useLocationStore
resetCart();     // resets useCartStore

QR Scanner

Camera-based QR code scanning with idempotency guards

Contextual Menus

Dynamic menu filtering by table type

Payments

Checkout flow and payment processing

Delivery Mode

Alternative mode for customers outside the geofence