Files
FamilyMealPlanner/CLAUDE.md
T
alexandrev-tibco 75c41b12ec comprobar el esquema de CloudKit antes de publicar
Un campo que esta en el @Model pero no en el esquema desplegado no sincroniza,
y no falla nada: los datos se quedan en el dispositivo que los escribio. Asi se
colaron nueve campos fuera de Production entre la 2.0 y la 2.1.2 sin que nadie
lo notara, incluidas las reglas de dia de las etiquetas y el desayuno/merienda
activados.

scripts/check_cloudkit_schema.py compara las propiedades almacenadas de cada
@Model con los CD_<campo> del esquema real (exportado con cktool), saltando
relaciones, computadas y el sufijo _ckAsset de los binarios.

fastlane submit y release abortan si falta algo; beta solo avisa, porque en
TestFlight es normal que el deploy a Production aun no se haya hecho. La lane
check_schema lo ejecuta suelto.

El deploy a Production sigue siendo manual: cktool no tiene subcomando y Apple
bloquea el endpoint de esquema en ese entorno.

Refs #38

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
2026-09-14 17:23:22 +02:00

124 lines
6.1 KiB
Markdown

# MealMood — Project Context for Claude Code
## App
- **Name**: MealMood: Family Meal Planner
- **Bundle ID**: com.alexandrevazquez.mealmood
- **Platform**: iOS/iPadOS (also runs as iOS app on Mac)
- **Current version**: 2.1.1 (dev; 2.1.0 with Apple Watch live in the App Store)
- **Main branch**: `1.0.1` (production), feature branches named after versions
### Branch ↔ version convention (IMPORTANT)
The active dev branch is named after the **current** `MARKETING_VERSION`, not the one it was created with. Whenever the iOS app version changes, **rename the branch to match**:
```bash
# after bumping MARKETING_VERSION in the Xcode project to e.g. 1.1.6
git branch -m <old-version> 1.1.6 # e.g. git branch -m 1.1.5 1.1.6
```
Right now: `MARKETING_VERSION = 2.1.1` → active branch is `2.1.1`. Keep them in sync. (The build number bump within the same version does NOT rename the branch — only a version change does.)
**`MARKETING_VERSION` in the Xcode project does NOT drive the shipped version.** Both Info.plists hardcode their values, so bumping the project setting alone builds an IPA with the old version and App Store Connect rejects it (`90062: must contain a higher version than that of the previously approved version`). A version bump means editing three places, and `fastlane`'s `increment_build_number` only handles the app's build number:
| What | Where | Bumped by fastlane? |
|------|-------|---------------------|
| App version | `MealMood/Resources/Info.plist``CFBundleShortVersionString` | ❌ by hand |
| App build | `MealMood/Resources/Info.plist` + pbxproj | ✅ |
| Widget version + build | `MealMoodWidget/Info.plist` | ❌ by hand |
The widget must match the app or the upload is rejected for a version mismatch. Since `fastlane beta` increments the build as its first step, set the widget to the **next** build number before running it:
```bash
plutil -replace CFBundleShortVersionString -string "<version>" MealMood/Resources/Info.plist
plutil -replace CFBundleShortVersionString -string "<version>" MealMoodWidget/Info.plist
plutil -replace CFBundleVersion -string "<next build>" MealMoodWidget/Info.plist
```
## Credentials — all managed via `pass` (GPG-encrypted, syncs to Gitea)
Never hardcode credentials. Always read from `pass`:
```bash
# Apple
pass show apple/mealmood/apple-id # alexandre.vazquez@gmail.com
pass show apple/mealmood/developer-team-id # 2825Q76T7H
pass show apple/mealmood/appstore-connect-team-id # 128443966
pass show apple/mealmood/app-store-app-id # 6759255553
pass show apple/mealmood/app-specific-password-fastlane # for fastlane uploads
# Firebase
pass show firebase/mealmood/api-key
pass show firebase/mealmood/project-id
pass show firebase/mealmood/google-app-id
# AdMob
pass show admob/mealmood/app-id
pass show admob/mealmood/banner-home-unit-id
```
### Antes de mandar una versión a la App Store: comprobar el esquema de CloudKit
Un campo que está en el `@Model` pero no en el esquema de CloudKit **no
sincroniza**, y no falla nada: los datos se quedan en el dispositivo que los
escribió. Así se colaron nueve campos fuera de Production entre la 2.0 y la
2.1.2 (issue #38) — desayuno/merienda activados, sus horas, el recordatorio,
las fotos del planificador y las reglas de día de las etiquetas no viajaban
entre dispositivos.
```bash
fastlane check_schema # o directamente:
python3 scripts/check_cloudkit_schema.py --environment production
```
`fastlane submit` y `fastlane release` ya lo ejecutan y **abortan** si falta
algo; `fastlane beta` solo avisa (en TestFlight aún puede faltar el deploy).
Si faltan campos: el esquema de development solo se actualiza al ejecutar la
app contra ese entorno con iCloud activo, o importándolo a mano:
```bash
TOKEN=$(pass show apple/mealmood/cloudkit-token)
TEAM=$(pass show apple/mealmood/developer-team-id)
xcrun cktool export-schema --token "$TOKEN" --team-id "$TEAM" \
--container-id iCloud.com.alexandrev.mealmood --environment development \
--output-file /tmp/schema.ckdb
# añadir los CD_<campo> que falten (Bool/Int → INT64, String → STRING,
# Date → TIMESTAMP, Data → CD_<campo>_ckAsset ASSET) y:
xcrun cktool import-schema --token "$TOKEN" --team-id "$TEAM" \
--container-id iCloud.com.alexandrev.mealmood --environment development \
--file /tmp/schema.ckdb
```
El paso a Production **no se puede automatizar**: `cktool` no tiene deploy y
Apple bloquea el endpoint de esquema en ese entorno. Lo hace Alexandre en
CloudKit Console → Schema → *Deploy Schema Changes* → Deploy to Production.
### TestFlight upload
```bash
export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=$(pass show apple/mealmood/app-specific-password-fastlane)
fastlane beta
```
## Stack
- SwiftUI + SwiftData (iOS 17+)
- Firebase Analytics (`AnalyticsService.swift`)
- Google AdMob (banner, free tier only, hidden on Mac)
- StoreKit 2 (subscription `com.mealmood.premium.monthly`)
- iCloud KV sync (`ICloudSyncService.swift`) — does NOT sync `isPremium`
- Widget extension (`MealMoodWidget/`) — requires App Group `group.com.alexandrevazquez.mealmood`
- Fastlane for CI/CD
## Premium logic
- `PremiumSyncService` is the source of truth for premium state transitions
- `isPremium` is determined **only** by StoreKit — never by iCloud sync
- `Transaction.updates` listener in `ContentView` catches purchases in real-time
- Tests: `MealMoodTests/PremiumSyncServiceTests.swift`
## Key files
| File | Purpose |
|------|---------|
| `MealMood/ContentView.swift` | App root, premium sync, Transaction.updates listener |
| `MealMood/Services/PremiumSyncService.swift` | Premium state machine (testable) |
| `MealMood/Services/StoreManager.swift` | StoreKit 2 purchase/restore |
| `MealMood/Services/ICloudSyncService.swift` | Cross-device sync (dishes, tags, plans — NOT isPremium) |
| `MealMood/Views/Home/WeekPlanShareView.swift` | Export styles (default, school, vertical A4) |
| `MealMoodWidget/MealMoodWidget.swift` | Home screen widget |
| `fastlane/Fastfile` | `beta` and `release` lanes |
| `fastlane/metadata/` | App Store metadata in 6 languages |