← All projects

PizzaV2

Cross-Platform Pizza Ordering

Modern pizza ordering app with phone auth, OTP verification, customizable sizes (S/M/L), real-time cart management, and order tracking with status badges. Built with React Native 0.76 and TypeScript.

iOS · Android
React NativeTypeScriptReact Navigation 7
The Problem

Building a modern food ordering app means juggling phone auth flows, complex product customization, and real-time cart state — all while delivering a polished native feel. Most solutions feel clunky, lack proper OTP UX, and fail to handle the nuances of mobile ordering like size variants, quantity management, and order lifecycle tracking.

The Solution

A dark-themed pizza ordering app with seamless phone authentication, OTP verification with auto-advancing inputs, a searchable catalog of 10 pizzas with S/M/L size options, real-time cart management, and full order history with status badges. Built from a single React Native CLI codebase targeting Android with Hermes engine.

Architecture

React Navigation 7 drives auth-gated routing with three navigators: `AuthNavigator` (onboarding → phone login → OTP), `TabNavigator` (home, history, cart, profile), and a nested stack for the pizza detail flow. State is managed through two React Context providers with guard-clause hooks. A fully custom `BottomNavigation` component replaces the default tab bar with emoji icons, active dot indicators, and a cart badge synced to context.

Design System

A cohesive dark design system with an orange (#FF6B03) accent, 20px border radius on cards, pill-shaped buttons, and consistent spacing tokens. Floating product images overlap cards with negative margins. Seven order status badges each have distinct color pairs — cooking (orange on dark orange), delivered (green on dark green), canceled (red on dark red) — powered by a token-based style map.

6
Key Features
3
Technologies
Cross-Platform Pizza Ordering
iOS · Android
Platforms

Key Features

1

Phone Auth & OTP

Three-step auth flow: onboarding → phone entry → 4-digit OTP. Individual input boxes with auto-advance on entry and backspace rewind — matching native SMS code behavior. Dynamic button states reflect input validity.

2

Searchable Catalog

Live search filters 10 pizzas by name or ingredient in real-time. Floating product cards with overlapping images and a "+" add-to-cart button. Empty state with friendly fallback.

3

Size Customization

S/M/L size selector with weight and price per option. Medium selected by default. Quantity counter with min-1 guard. Total price derived from selected option × quantity.

4

Immutable Cart

Context-driven cart with `map` + `filter` immutable update pattern. Quantity overflow auto-removes items at zero. Badge counter syncs across screens via custom tab bar. Derived `totalCount` and `totalPrice` — no stale state.

5

Order Status Badges

Seven-status enum (Completed, Delivered, Pending, Cooking, On the Way, Arrived, Canceled) with token-based color mapping. Each badge renders with semantic label, text color, and background — no conditional styling.

6

Custom Tab Bar

Full reimplementation of `BottomTabBarProps` with emoji icons, active dot indicator, and absolutely-positioned cart badge. Floating design with 30px border radius and 80px height.

Under the hood

Code
import React, { useRef, useState } from 'react';
import {
  View, TextInput, Text, KeyboardAvoidingView,
  Platform, ScrollView, TouchableOpacity,
} from 'react-native';
import { useRoute, RouteProp } from '@react-navigation/native';
import AppButton from '../components/AppButton';
import { useAuth } from '../context/AuthContext';
import { AuthStackParamList } from '../navigation/AuthNavigator';

type Route = RouteProp<AuthStackParamList, 'OtpVerification'>;
const OTP_LENGTH = 4;

const OtpVerificationScreen = () => {
  const [otp, setOtp] = useState<string[]>(Array(OTP_LENGTH).fill(''));
  const inputs = useRef<(TextInput | null)[]>([]);
  const { login } = useAuth();
  const route = useRoute<Route>();
  const phone = route.params?.phone ?? '+1 555 123 4567';

  const handleChange = (text: string, index: number) => {
    if (text.length > 1) return;
    const newOtp = [...otp];
    newOtp[index] = text;
    setOtp(newOtp);
    if (text && index < OTP_LENGTH - 1) {
      inputs.current[index + 1]?.focus();
    }
  };

  const handleKeyPress = (e: any, index: number) => {
    if (e.nativeEvent.key === 'Backspace' && !otp[index] && index > 0) {
      inputs.current[index - 1]?.focus();
    }
  };

  const isComplete = otp.every(d => d !== '');

  return (
    <KeyboardAvoidingView
      style={{ flex: 1, backgroundColor: '#0F0F0F' }}
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
    >
      <ScrollView
        contentContainerStyle={{
          flexGrow: 1, alignItems: 'center',
          paddingHorizontal: 20, paddingVertical: 40,
        }}
        keyboardShouldPersistTaps="handled"
      >
        <Text style={{ color: 'white', fontSize: 40, fontWeight: 'bold', marginTop: 20 }}>
          SMS Code
        </Text>
        <Text style={{ color: '#9EA1AB', fontSize: 18, marginTop: 10, textAlign: 'center' }}>
          We sent a 4-digit code to {'\n'}
          <Text style={{ color: 'white' }}>{phone}</Text>
        </Text>

        <View style={{ flexDirection: 'row', gap: 12, marginTop: 40, marginBottom: 20 }}>
          {otp.map((digit, index) => (
            <TextInput
              key={index}
              ref={ref => (inputs.current[index] = ref)}
              style={{
                width: 75, height: 75, backgroundColor: '#121212',
                borderRadius: 15, borderWidth: 2,
                borderColor: digit ? '#FF6B00' : '#262626',
                fontSize: 26, fontWeight: 'bold', color: 'white',
                textAlign: 'center',
              }}
              value={digit}
              onChangeText={text => handleChange(text, index)}
              onKeyPress={e => handleKeyPress(e, index)}
              keyboardType="numeric"
              maxLength={1}
              selectionColor="#FF6B00"
              caretHidden
            />
          ))}
        </View>

        <View style={{ flex: 1, minHeight: 40 }} />

        <AppButton
          onPress={login}
          title="Verify"
          containerColor={isComplete ? '#FF6B00' : '#3A3A3A'}
          contentColor="white"
        />
      </ScrollView>
    </KeyboardAvoidingView>
  );
};

Gallery

Want to see more?

Explore the full project catalog or grab the source code.