Mobile Components

The mobile app uses a set of reusable React Native components for a consistent user experience across iOS and Android.


Component Hierarchy

App.tsx
ā”œā”€ā”€ AuthProvider
│   └── PreferencesProvider
│       └── ThemeProvider
│           └── {Auth State}
│               ā”œā”€ā”€ [Not Signed In] → LoginScreen
│               └── [Signed In/Guest] → HomeScreen
│                   ā”œā”€ā”€ ProfileAvatar
│                   ā”œā”€ā”€ ProfileSelector
│                   │   └── PrivacyPolicyModal
│                   ā”œā”€ā”€ ImageCapture
│                   ā”œā”€ā”€ ResultsHeader
│                   │   └── RiskBadge
│                   └── IngredientCard
│                       └── SafetyBar

ImageCapture

Camera interface for capturing ingredient labels with real-time preview and gallery selection.

Usage

<ImageCapture
  onCapture={(base64Image) => handleCapture(base64Image)}
  onCancel={() => setShowCamera(false)}
/>

Props

PropTypeDescription
onCapture(base64: string) => voidCalled with captured image
onCancel() => voidCalled when user cancels

Features

  • Real-time camera viewfinder
  • Tap-to-capture
  • Gallery image selection
  • Automatic orientation handling
  • Base64 image encoding

Implementation

// Camera capture
const takePicture = async () => {
  if (cameraRef.current) {
    const photo = await cameraRef.current.takePictureAsync({
      base64: true,
      quality: 0.8,
    });
    onCapture(photo.base64);
  }
};

// Gallery selection
const pickImage = async () => {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ImagePicker.MediaTypeOptions.Images,
    base64: true,
    quality: 0.8,
  });

  if (!result.canceled && result.assets[0].base64) {
    onCapture(result.assets[0].base64);
  }
};

IngredientCard

Expandable card showing ingredient safety details with visual indicators.

Usage

<IngredientCard ingredient={ingredientDetail} />

Visual Design

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ āš ļø Fragrance               4/10 ◐  │  ← Collapsed
│ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘  │
│ Scent, masking agent           ā–¼   │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ PURPOSE                             │  ← Expanded
│ Provides scent, masks other odors   │
│                                     │
│ ORIGIN                              │
│ Synthetic                           │
│                                     │
│ CONCERNS                            │
│ Common allergen, may cause...       │
│                                     │
│ RECOMMENDATION                      │
│ [CAUTION]                           │
│                                     │
│ Category: Cosmetics  Allergy: High  │
│                                     │
│ SAFER ALTERNATIVES                  │
│ [fragrance-free] [essential oils]   │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Score Visualization

ScoreColorIndicator
8-10 Greenāœ“
6-7 Light Greenā—
4-5 Amber◐
1-3 Red!

ProfileAvatar

Displays user's Google profile picture or a colored initial fallback.

Usage

<ProfileAvatar
  user={user}
  size={48}
  onPress={() => setCurrentScreen('profile')}
/>

Props

PropTypeDefaultDescription
userUser | null-Firebase user object
sizenumber40Avatar diameter in pixels
onPress() => void-Optional tap handler

Behavior

  • Authenticated with photo: Displays Google profile picture
  • Authenticated without photo: Shows first initial with colored background
  • Guest mode: Shows "G" with colored background

Color Generation

Background color is generated from a hash of the user's UID or email for consistent colors:

const getColorFromString = (str: string): string => {
  const colors = [
    '#ef4444', '#f97316', '#f59e0b', '#84cc16',
    '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6',
    '#8b5cf6', '#a855f7', '#ec4899',
  ];
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
  }
  return colors[Math.abs(hash) % colors.length];
};

ProfileSelector

User profile configuration with authentication, preferences, and account management.

Features

  • User Profile Display: ProfileAvatar, name, and email for authenticated users
  • Dark/Light mode toggle: Theme switching with persistence
  • Known allergies: Multi-select (Fragrance, Sulfates, Parabens, etc.)
  • Skin type: Single select (Normal, Dry, Oily, Combination, Sensitive)
  • Explanation style: Simple (beginner) or Technical (expert)
  • Privacy Policy: In-app modal viewer
  • Account Management: Sign out and delete account options

UI Layout

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Your Profile                    āœ•   │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ [Avatar] John Doe                   │
│          john@example.com           │
│                        [Sign Out]   │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Appearance                          │
│ ā˜€ļø Light          šŸŒ™ Dark  [ā—ā”ā”ā”ā—‹]  │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Known Allergies                     │
│ [Fragrance āœ“] [Sulfates] [Parabens] │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Skin Type                           │
│ ā—‹ Normal  ā—‹ Dry  ā— Sensitive        │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Explanation Style                   │
│ ā— Simple (beginner)                 │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ [Privacy Policy]                    │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ ā–¼ Danger Zone                       │
│ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │
│ │ [Delete Account]                │ │
│ │ This will delete all your data  │ │
│ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Danger Zone

Account deletion is protected by a collapsible section to prevent accidental clicks:

const [showDangerZone, setShowDangerZone] = useState(false);

const toggleDangerZone = () => {
  LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
  setShowDangerZone(!showDangerZone);
};

ResultsHeader

Analysis summary with overall risk assessment and allergen warnings.

Usage

<ResultsHeader
  productName="CeraVe Moisturizer"
  overallRisk="low"
  averageSafetyScore={8.2}
  ingredientCount={12}
  allergenWarnings={[]}
/>

Visual Design

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ CeraVe Moisturizer                  │
│                                     │
│ Overall Risk: LOW                   │
│ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘  8.2/10        │
│                                     │
│ 12 Ingredients Analyzed             │
│                                     │
│ āš ļø 1 Allergen Warning               │
│ Fragrance matches your sensitivity  │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

SafetyBar & RiskBadge

SafetyBar

Horizontal safety score visualization:

<SafetyBar score={8} maxScore={10} />

// Color mapping
const getScoreColor = (score: number): string => {
  if (score >= 8) return '#22c55e'; // Green
  if (score >= 6) return '#84cc16'; // Light green
  if (score >= 4) return '#f59e0b'; // Amber
  if (score >= 2) return '#f97316'; // Orange
  return '#ef4444'; // Red
};

RiskBadge

Risk level indicator badge:

<RiskBadge level="low" />
<RiskBadge level="medium" />
<RiskBadge level="high" />
LevelBackgroundText
Low#dcfce7#166534
Medium#fef9c3#854d0e
High#fee2e2#991b1b

Type Definitions

// types/index.ts

export interface UserProfile {
  allergies: string[];
  skinType: SkinType;
  expertise: ExpertiseLevel;
}

export type SkinType = 'normal' | 'dry' | 'oily' | 'combination' | 'sensitive';
export type ExpertiseLevel = 'beginner' | 'expert';
export type ThemeMode = 'light' | 'dark';
export type RiskLevel = 'low' | 'medium' | 'high';

export interface IngredientDetail {
  name: string;
  purpose: string;
  safety_score: number;
  risk_level: RiskLevel;
  concerns: string;
  recommendation: string;
  origin: string;
  category: string;
  allergy_risk: string;
  is_allergen_match: boolean;
  alternatives: string[];
}

Related Documentation