Health-Score

CLAUDE.md

Guidance for Claude Code (and humans) working in this repo. Read this first. For detailed conventions and “do not touch” rules, also see AGENTS.md. For shipping to the stores, see STORE_RELEASE.md.

What this app is

Sağlık Skoru — an Expo / React Native (SDK 55, new architecture) mobile app that scans a product’s ingredient label (camera/gallery) or barcode and returns an AI-generated health score, ingredient breakdown, allergens, and a verdict. Backed by Firebase (Auth + Firestore + Cloud Functions). Monetized with RevenueCat (subscriptions + token packs) and AdMob (rewarded ads). i18n in 6 languages. Gamification: tokens, badges, streaks, weekly leaderboard.

Architecture map

Core data flow: product analysis (single pipeline)

There is one analysis path. Do not reintroduce parallel ones.

  1. ScanScreen → analyzeProduct() in src/services/analyze.ts (vision mode with an image, OR text mode with productName + ingredientsText from OpenFoodFacts for barcodes).
  2. analyze.ts calls the analyzeProduct Cloud Function (functions/src/productScoring.ts).
  3. The function caches results in Firestore products/{cacheKey}_{LANG}:
    • real barcode → keyed by barcode (shared cache),
    • manual/image scan → keyed by img_<sha256(image)> (so identical photos hit cache, different photos never collide — this fixed the old manual_TR collision bug).
  4. On cache miss it calls Gemini (gemini-2.5-flash, JSON-only) and persists. Invalid / low-confidence (<80) results get save_rejected: true and are NOT saved.
  5. analyze.ts bridges the result to the UI shape; ScanScreen navigates to ResultScreen with analysisResult. ResultScreen only reads route.params.analysisResult — never pass it an analyzeInput and expect it to run analysis (the old broken barcode path did).

The Gemini schema returns: is_valid_product, confidence_score, overall_health_score, product_name, sub_scores, dietary_flags, allergens, ingredients[], analysis_points{pros,cons}, healthy_alternative, summary_text.

The token / premium economy is SERVER-SIDE — do not bypass it

A user can NOT write their own tokens, premiumUntil, isAdmin, badges, status, etc. firestore.rules only lets an owner update a safe allow-list of fields (athleteMode, allergies, displayName, photoURL, language, …). All value-bearing mutations go through Cloud Function callables (which run with admin privileges):

Action Callable Client entry
Scan charge (5 premium/trial, 10 free) inside analyzeProduct analyze.ts (throws insufficient-tokens)
Daily login reward (24h cooldown) claimDailyReward useToken().claimDailyReward()
Rewarded-ad credit (rate-limited) claimAdReward useToken().claimAdReward()
7-day streak reward (~weekly cooldown) claimStreakReward streak.ts
Unlock badge + bonus (idempotent) grantBadge useToken().grantBadge(id)
Weekly winner reward claimWeeklyReward useToken().claimWeeklyReward()
Redeem promo code redeemPromo promoCode.ts
Premium days (sim/admin only) grantPremiumDays useToken().setIsPremium(true, days, bonus)
Token pack (sim/admin only) grantTokenPack useToken().purchaseTokens(amount, packId)
Delete account (store requirement) deleteAccount profile modal in ScanScreen

Reward amounts and cooldowns are constants in functions/src/index.ts. The tokens UI value refreshes via the Firestore onSnapshot listener after a callable runs — clients no longer optimistically write it.

Premium source of truth: derived from THREE inputs in TokenContext — Firestore premiumUntil (admin/promo grants), RevenueCat entitlement (rcPremium, real IAP), or active trial. In production, real purchase premium comes from the RevenueCat entitlement, not a Firestore write (grantPremiumDays is disabled in prod unless admin).

Monetization toggles

Environment variables (EXPO_PUBLIC_ prefix required)

Firestore collections

Commands

# App
npm start                 # Metro / Expo dev server
npm run android | ios     # native run (dev build)
npx tsc --noEmit          # typecheck (must stay at 0 errors)
npx expo lint             # eslint

# Functions
cd functions
npm run build             # tsc
npm run serve             # emulator (pair with EXPO_PUBLIC_USE_EMULATOR=1 on the client)
npm run deploy            # firebase deploy --only functions

# Security rules
firebase deploy --only firestore:rules

⚠️ npm run deploy can silently skip your changes. Firebase decides what to redeploy from a firebase-functions-hash label on each deployed function, and that hash does not always change when functions/src does — a bare firebase deploy --only functions then prints Skipped (No changes detected) for every function and your fix never ships. Deploying a NAMED function bypasses the check:

firebase deploy --only functions:analyzeProduct,functions:getReferralCode

Always verify what actually landed — a skipped deploy still exits 0:

gcloud functions describe <name> --gen2 --region=us-central1 \
  --project=saglikskoru-4d0a8 --format="value(updateTime)"

iOS build config (do not “simplify” this)

Android native libs

Conventions (see AGENTS.md for the full list)

Telemetry (Crashlytics + Analytics)

src/services/telemetry.ts wraps @react-native-firebase/crashlytics + /analytics (NATIVE modules — auto no-op in Expo Go / web via lazy-require guards). initTelemetry() runs at startup (App.tsx), routes uncaught JS errors to Crashlytics, and enables collection; setTelemetryUser(uid) ties reports to the user; logEvent() records events (scan_completed, purchase_tokens, purchase_premium). Config plugins are in app.json.

Scheduled functions

Current state (handoff to a fresh session)

Before production / open user actions

Known follow-ups (not yet done)