Skip to main content

Components

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

Component Hierarchyโ€‹

ImageCaptureโ€‹

Camera interface for capturing ingredient labels.

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 Notesโ€‹

// 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.

Usageโ€‹

<IngredientCard ingredient={ingredientDetail} />

Propsโ€‹

PropTypeDescription
ingredientIngredientDetailIngredient data object

Displaysโ€‹

Collapsed View:

  • Ingredient name with safety score
  • Color-coded safety bar
  • Purpose tag
  • Expand/collapse arrow

Expanded View:

  • Full purpose description
  • Origin (Natural/Synthetic)
  • Concerns (if any)
  • Recommendation badge (SAFE/CAUTION/AVOID)
  • Category and Allergy Risk
  • Safer alternatives

Visual Designโ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ โš ๏ธ Fragrance 4/10 โ— โ”‚
โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ โ”‚
โ”‚ Scent, masking agent โ–ผ โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ PURPOSE โ”‚
โ”‚ 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โ€‹

ScoreColorEmoji
8-10Green (#22c55e)โœ“
6-7Light Green (#84cc16)โ—
4-5Amber (#f59e0b)โ—
1-3Red (#ef4444)!

ProfileAvatarโ€‹

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

Usageโ€‹

import { ProfileAvatar } from '../components/ProfileAvatar';

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

Propsโ€‹

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

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โ€‹

The background color is generated from a hash of the user's UID or email, ensuring consistent colors across sessions:

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.

Usageโ€‹

<ProfileSelector
profile={userProfile}
onProfileChange={setUserProfile}
/>

Propsโ€‹

PropTypeDescription
profileUserProfileCurrent profile state
onProfileChange(profile: UserProfile) => voidProfile update handler

Featuresโ€‹

  • User Profile Display: Shows 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

Danger Zoneโ€‹

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

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

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

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 โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ResultsHeaderโ€‹

Analysis summary with overall risk assessment.

Usageโ€‹

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

Propsโ€‹

PropTypeDescription
productNamestringAnalyzed product name
overallRiskRiskLevelOverall risk assessment
averageSafetyScorenumberAverage score (1-10)
ingredientCountnumberTotal ingredients
allergenWarningsstring[]Allergen warning messages

Visual Designโ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ CeraVe Moisturizer โ”‚
โ”‚ โ”‚
โ”‚ Overall Risk: LOW โ”‚
โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘ 8.2/10 โ”‚
โ”‚ โ”‚
โ”‚ 12 Ingredients Analyzed โ”‚
โ”‚ โ”‚
โ”‚ โš ๏ธ 1 Allergen Warning โ”‚
โ”‚ Fragrance matches your sensitivity โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

SafetyBarโ€‹

Horizontal safety score visualization.

Usageโ€‹

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

Propsโ€‹

PropTypeDefaultDescription
scorenumber-Current score
maxScorenumber10Maximum score
heightnumber6Bar height in pixels

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.

Usageโ€‹

<RiskBadge level="low" />
<RiskBadge level="medium" />
<RiskBadge level="high" />

Propsโ€‹

PropTypeDescription
levelRiskLevelRisk level to display

Stylingโ€‹

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[];
}

PrivacyPolicyModalโ€‹

In-app modal displaying the privacy policy without leaving the app.

Usageโ€‹

const [showPrivacy, setShowPrivacy] = useState(false);

<TouchableOpacity onPress={() => setShowPrivacy(true)}>
<Text>Privacy Policy</Text>
</TouchableOpacity>

<PrivacyPolicyModal
visible={showPrivacy}
onClose={() => setShowPrivacy(false)}
/>

Propsโ€‹

PropTypeDescription
visiblebooleanControls modal visibility
onClose() => voidCalled when user closes modal

Featuresโ€‹

  • Full privacy policy content displayed in-app
  • Works offline (no external URL required)
  • ScrollView for long content
  • Theme-aware styling