Skip to main content

Authentication

The AI Ingredient Scanner uses Firebase Authentication for user management, with Google Sign-In as the primary authentication method.

Overview

Features

FeatureDescription
Google Sign-InOAuth 2.0 authentication via Firebase
Guest ModeAnonymous usage without account
Profile SyncFirestore persistence across devices
Preferences SyncAllergies, skin type, theme saved to cloud
Account DeletionGDPR-compliant data removal

Authentication Flow

1. Initial State

When the app launches, it checks for an existing Firebase session:

// AuthContext.tsx
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
setUser(firebaseUser);
if (firebaseUser) {
await syncUserToFirestore(firebaseUser);
}
setLoading(false);
});
return () => unsubscribe();
}, []);

2. Login Screen

Users see a premium login screen with two options:

  • Continue with Google - Full authentication with profile sync
  • Continue as Guest - Anonymous mode (no data persistence)

3. Google Sign-In

Platform-specific implementation:

const signInWithGoogle = async () => {
if (Platform.OS === 'web') {
// Web: Firebase popup
await signInWithPopup(auth, googleProvider);
} else {
// Native: Expo Auth Session
await promptAsync();
}
};

4. Firestore Profile Sync

On successful sign-in, user data syncs to Firestore:

// User document structure
interface FirestoreUser {
uid: string;
email: string | null;
displayName: string | null;
photoURL: string | null;
provider: string;
createdAt: Timestamp;
lastLoginAt: Timestamp;
preferences?: {
allergies: string[];
skinType: SkinType;
expertise: ExpertiseLevel;
theme: ThemeMode;
};
}

Preferences Sync

User preferences are managed by PreferencesContext and automatically synced:

Synced Preferences

PreferenceDescriptionDefault
allergiesKnown allergens to flag[]
skinTypeSkin type for cosmetic analysisnormal
expertiseExplanation complexitybeginner
themeLight or dark modelight

Load on Login

When a user signs in, their preferences are loaded from Firestore:

// PreferencesContext.tsx
useEffect(() => {
if (user) {
const userRef = doc(db, 'users', user.uid);
const userSnap = await getDoc(userRef);

if (userSnap.exists()) {
const prefs = userSnap.data().preferences;
setPreferences(prefs);
setThemeMode(prefs.theme); // Sync with ThemeContext
}
}
}, [user]);

Auto-Save on Change

Preferences are saved with debouncing (1 second delay) to prevent excessive writes:

const debouncedSave = useCallback((newPrefs: UserPreferences) => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}

saveTimeoutRef.current = setTimeout(async () => {
if (user) {
await setDoc(doc(db, 'users', user.uid),
{ preferences: newPrefs },
{ merge: true }
);
}
}, 1000);
}, [user]);

Firebase Configuration

Setup

Firebase is configured in src/config/firebase.ts:

const firebaseConfig = {
apiKey: 'your-api-key',
authDomain: 'your-project.firebaseapp.com',
projectId: 'your-project-id',
storageBucket: 'your-project.appspot.com',
messagingSenderId: 'your-sender-id',
appId: 'your-app-id',
measurementId: 'your-measurement-id',
};

OAuth Configuration

For Google Sign-In to work:

  1. Firebase Console: Enable Google provider in Authentication
  2. Google Cloud Console: Configure OAuth consent screen
  3. Web Client ID: Add to AuthContext.tsx
const [request, response, promptAsync] = Google.useAuthRequest({
webClientId: 'your-oauth-client-id.apps.googleusercontent.com',
});

User Management

Sign Out

const signOut = async () => {
await firebaseSignOut(auth);
setUserProfile(null);
};

Account Deletion

GDPR-compliant deletion removes all user data:

const deleteAccount = async () => {
// 1. Delete scan history subcollection
const scansRef = collection(db, 'users', user.uid, 'scans');
const scansSnap = await getDocs(scansRef);

const batch = writeBatch(db);
scansSnap.docs.forEach((scanDoc) => {
batch.delete(scanDoc.ref);
});

// 2. Delete user document
batch.delete(doc(db, 'users', user.uid));
await batch.commit();

// 3. Delete Firebase auth user
await deleteUser(user);
};

Firestore Security Rules

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can only access their own data
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;

// Scan history subcollection
match /scans/{scanId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
}

UI Components

Login Screen

Premium gradient design with:

  • App branding and logo
  • Feature pills (Scan Labels, AI Analysis, Allergy Alerts)
  • Google Sign-In button
  • Guest mode option
  • Privacy policy link

ProfileAvatar

Displays user's profile picture or a colored initial fallback:

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

Behavior:

  • With Google photo: Shows the profile picture from user.photoURL
  • Without photo: Shows first initial of display name with colored background
  • Guest mode: Shows "G" with colored background

The background color is deterministically generated from the user's UID for consistency across sessions.

Profile Section (ProfileSelector)

In Settings, authenticated users see:

  • ProfileAvatar: Google photo or colored initial fallback
  • Display name and email address
  • Sign Out button
  • Privacy Policy (in-app modal via PrivacyPolicyModal)
  • Collapsible Danger Zone with Delete Account option

Guest users see:

  • Guest mode avatar with "G" initial
  • Sign In with Google button
  • Privacy Policy link

Danger Zone Pattern

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

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

// Toggle with smooth animation
const toggleDangerZone = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setShowDangerZone(!showDangerZone);
};

The Danger Zone is collapsed by default. Users must tap to expand before seeing the Delete Account button.

PrivacyPolicyModal

In-app modal that displays the privacy policy without leaving the app:

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

Benefits:

  • Works offline (no external URL required)
  • Keeps users in the app
  • Theme-aware styling

Privacy

The privacy policy is displayed in-app via PrivacyPolicyModal. It covers:

  • Data collection and usage
  • User rights (access, update, delete)
  • Third-party services (Firebase, Google)
  • Data retention policies