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
ā āāā SafetyBarImageCapture
Camera interface for capturing ingredient labels with real-time preview and gallery selection.
Usage
<ImageCapture
onCapture={(base64Image) => handleCapture(base64Image)}
onCancel={() => setShowCamera(false)}
/>Props
| Prop | Type | Description |
|---|---|---|
| onCapture | (base64: string) => void | Called with captured image |
| onCancel | () => void | Called 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
| Score | Color | Indicator |
|---|---|---|
| 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
| Prop | Type | Default | Description |
|---|---|---|---|
| user | User | null | - | Firebase user object |
| size | number | 40 | Avatar 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" />
| Level | Background | Text |
|---|---|---|
| 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[];
}