hogar compartido: colaboracion entre cuentas sobre Firestore

CloudKit sincroniza la base privada de un Apple ID: ni llega a Android ni deja
que dos cuentas editen el mismo plan (CKShare sigue sin existir en SwiftData).
El contenido de un hogar pasa por tanto a Firestore, y un dispositivo que entra
en un hogar construye el store local sin CloudKit — dos espejos escribiendo los
mismos objetos se pelean, que es justo lo que ya obligó a apagar el sync por
iCloud KV.

SwiftData sigue siendo el store local y el modo offline; HouseholdSyncService es
lo unico que habla con la red. Detecta cambios comparando una huella del
contenido de cada documento con la ultima sincronizada (el "shadow"), asi que no
hace falta instrumentar con updatedAt las treinta vistas que mutan modelos. Los
borrados van como tombstone: un borrado duro volveria desde cualquier miembro
que estuviera sin conexion.

Semanas y slots usan id derivado del contenido (2026-09-14, 5-dinner) para que
dos miembros que abren la misma semana escriban el mismo documento en vez de
crear dos, y para que los conflictos se resuelvan por slot y no por semana.

Incluye reglas de seguridad (solo miembros; los codigos de invitacion se pueden
leer por id pero no listar), pantalla de hogar en Ajustes con Sign in with Apple,
invitacion por codigo de 6 caracteres sin vocales ni 0/O/1/I, y la eleccion al
unirse entre llevarse los platos propios o adoptar los del hogar.

Fuera de esta fase: fotos de platos (necesitan Storage) y el cliente Android.

Refs #33

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
This commit is contained in:
alexandrev-tibco
2026-09-12 13:00:38 +02:00
parent 66128e7a5c
commit 163fd6026a
22 changed files with 2261 additions and 8 deletions
+4
View File
@@ -14,6 +14,10 @@
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.alexandrevazquez.mealmood</string>
@@ -478,3 +478,40 @@
"week_schedule_warning_confirm" = "Trotzdem entfernen";
"toast_week_schedule_updated" = "Woche aktualisiert";
"toast_week_schedule_reset" = "Woche folgt wieder deinen Einstellungen";
/* Hogar compartido (2.2) */
"household_title" = "Gemeinsamer Haushalt";
"household_section" = "Haushalt";
"household_settings_footer" = "Plant die Woche gemeinsam, jede Person mit eigenem Konto. Funktioniert auch unter Android.";
"household_intro" = "In einem Haushalt planen mehrere Personen dieselbe Woche und teilen sich Gerichte und Einkaufsliste.";
"household_sign_in_footer" = "Zum Teilen musst du dich anmelden. Deine Gerichte bleiben deine: außerhalb deines Haushalts sieht sie niemand.";
"household_create_section" = "Haushalt erstellen";
"household_create" = "Haushalt erstellen";
"household_name_placeholder" = "Name des Haushalts (z. B. Zuhause)";
"household_create_footer" = "Deine aktuellen Gerichte, Tags und Wochen werden zum Ausgangspunkt des Haushalts.";
"household_create_premium_footer" = "Einen Haushalt zu erstellen ist Premium. Wer eingeladen wird, zahlt nichts.";
"household_join_section" = "Haushalt beitreten";
"household_join" = "Mit Code beitreten";
"household_code_placeholder" = "Einladungscode";
"household_join_footer" = "Frag die Person, die den Haushalt erstellt hat, nach dem Code. Er läuft nach 7 Tagen ab.";
"household_join_choice_title" = "Was ist mit deinen Gerichten?";
"household_join_choice_message" = "Du kannst deine Gerichte und Wochen in diesen Haushalt mitnehmen oder nur mit denen des Haushalts starten.";
"household_join_choice_merge" = "Meine Gerichte mitnehmen";
"household_join_choice_replace" = "Nur die des Haushalts";
"household_members_section" = "Mitglieder";
"household_member_you" = "du";
"household_member_unnamed" = "Ohne Namen";
"household_invite_section" = "Einladung";
"household_invite_footer" = "Teile diesen Code mit allen, die mit dir planen sollen.";
"household_invite_share" = "Komm in meinen MealMood-Haushalt mit dem Code %@ — https://mealmood.app";
"household_invite_expires" = "Läuft ab am %@";
"household_invite_regenerate" = "Neuen Code erzeugen";
"household_leave" = "Haushalt verlassen";
"household_leave_confirm_title" = "Haushalt verlassen?";
"household_leave_confirm_message" = "Du siehst und bearbeitest die Gerichte und Wochen des Haushalts auf diesem Gerät nicht mehr.";
"household_leave_confirm_confirm" = "Verlassen";
"household_relaunch_needed" = "Schließe MealMood und öffne es erneut, um den Wechsel abzuschließen.";
"household_error_not_signed_in" = "Melde dich an, bevor du einen Haushalt erstellst oder beitrittst.";
"household_error_no_household" = "Dieses Gerät gehört zu keinem Haushalt.";
"household_error_invalid_code" = "Diesen Code gibt es nicht.";
"household_error_expired_code" = "Dieser Code ist abgelaufen. Bitte um einen neuen.";
@@ -478,3 +478,40 @@
"week_schedule_warning_confirm" = "Remove anyway";
"toast_week_schedule_updated" = "Week updated";
"toast_week_schedule_reset" = "Week back to your general settings";
/* Hogar compartido (2.2) */
"household_title" = "Shared household";
"household_section" = "Household";
"household_settings_footer" = "Plan the week together, each person from their own account. Works from Android too.";
"household_intro" = "A household lets several people plan the same week, sharing dishes and the shopping list.";
"household_sign_in_footer" = "Signing in is what makes sharing possible. Your dishes stay yours: nobody outside your household sees them.";
"household_create_section" = "Create a household";
"household_create" = "Create household";
"household_name_placeholder" = "Household name (e.g. Home)";
"household_create_footer" = "Your current dishes, tags and weeks become the household's starting point.";
"household_create_premium_footer" = "Creating a household is Premium. Whoever you invite doesn't have to pay.";
"household_join_section" = "Join a household";
"household_join" = "Join with code";
"household_code_placeholder" = "Invite code";
"household_join_footer" = "Ask whoever created the household for the code. It expires after 7 days.";
"household_join_choice_title" = "What about your dishes?";
"household_join_choice_message" = "You can bring the dishes and weeks you already have into this household, or start with the household's own.";
"household_join_choice_merge" = "Bring my dishes";
"household_join_choice_replace" = "Use the household's only";
"household_members_section" = "Members";
"household_member_you" = "you";
"household_member_unnamed" = "Unnamed";
"household_invite_section" = "Invitation";
"household_invite_footer" = "Share this code with whoever you want planning with you.";
"household_invite_share" = "Join my MealMood household with the code %@ — https://mealmood.app";
"household_invite_expires" = "Expires on %@";
"household_invite_regenerate" = "Generate a new code";
"household_leave" = "Leave household";
"household_leave_confirm_title" = "Leave the household?";
"household_leave_confirm_message" = "You'll stop seeing and editing the household's dishes and weeks on this device.";
"household_leave_confirm_confirm" = "Leave";
"household_relaunch_needed" = "Quit and reopen MealMood to finish the change.";
"household_error_not_signed_in" = "Sign in before creating or joining a household.";
"household_error_no_household" = "This device doesn't belong to a household.";
"household_error_invalid_code" = "That code doesn't exist.";
"household_error_expired_code" = "That code has expired. Ask for a new one.";
@@ -478,3 +478,40 @@
"week_schedule_warning_confirm" = "Quitar igualmente";
"toast_week_schedule_updated" = "Semana actualizada";
"toast_week_schedule_reset" = "Semana con tu configuración general";
/* Hogar compartido (2.2) */
"household_title" = "Hogar compartido";
"household_section" = "Hogar";
"household_settings_footer" = "Planificad la semana entre varias personas, cada una desde su cuenta. Funciona también desde Android.";
"household_intro" = "Un hogar permite que varias personas planifiquen la misma semana, con sus platos y su lista de la compra en común.";
"household_sign_in_footer" = "Necesitas identificarte para compartir con otra persona. Tus platos siguen siendo tuyos: nadie fuera de tu hogar los ve.";
"household_create_section" = "Crear un hogar";
"household_create" = "Crear hogar";
"household_name_placeholder" = "Nombre del hogar (p. ej. Casa)";
"household_create_footer" = "Tus platos, etiquetas y semanas actuales pasan a ser el punto de partida del hogar.";
"household_create_premium_footer" = "Crear un hogar es Premium. Quien recibe la invitación no necesita pagar.";
"household_join_section" = "Unirme a un hogar";
"household_join" = "Unirme con el código";
"household_code_placeholder" = "Código de invitación";
"household_join_footer" = "Pide el código a quien creó el hogar. Caduca a los 7 días.";
"household_join_choice_title" = "¿Qué hacemos con tus platos?";
"household_join_choice_message" = "Puedes llevarte a este hogar los platos y semanas que ya tienes, o empezar solo con los del hogar.";
"household_join_choice_merge" = "Llevarme mis platos";
"household_join_choice_replace" = "Usar solo los del hogar";
"household_members_section" = "Miembros";
"household_member_you" = "tú";
"household_member_unnamed" = "Sin nombre";
"household_invite_section" = "Invitación";
"household_invite_footer" = "Comparte este código con quien quieras que planifique contigo.";
"household_invite_share" = "Únete a mi hogar en MealMood con el código %@ — https://mealmood.app";
"household_invite_expires" = "Caduca el %@";
"household_invite_regenerate" = "Generar código nuevo";
"household_leave" = "Salir del hogar";
"household_leave_confirm_title" = "¿Salir del hogar?";
"household_leave_confirm_message" = "Dejarás de ver y de editar los platos y las semanas del hogar en este dispositivo.";
"household_leave_confirm_confirm" = "Salir";
"household_relaunch_needed" = "Cierra y vuelve a abrir MealMood para terminar el cambio.";
"household_error_not_signed_in" = "Identifícate antes de crear o unirte a un hogar.";
"household_error_no_household" = "Este dispositivo no pertenece a ningún hogar.";
"household_error_invalid_code" = "Ese código no existe.";
"household_error_expired_code" = "Ese código ha caducado. Pide uno nuevo.";
@@ -478,3 +478,40 @@
"week_schedule_warning_confirm" = "Retirer quand même";
"toast_week_schedule_updated" = "Semaine mise à jour";
"toast_week_schedule_reset" = "Semaine revenue à vos réglages généraux";
/* Hogar compartido (2.2) */
"household_title" = "Foyer partagé";
"household_section" = "Foyer";
"household_settings_footer" = "Planifiez la semaine à plusieurs, chacun depuis son compte. Fonctionne aussi depuis Android.";
"household_intro" = "Un foyer permet à plusieurs personnes de planifier la même semaine, avec les plats et la liste de courses en commun.";
"household_sign_in_footer" = "Il faut se connecter pour partager. Vos plats restent les vôtres : personne en dehors de votre foyer ne les voit.";
"household_create_section" = "Créer un foyer";
"household_create" = "Créer le foyer";
"household_name_placeholder" = "Nom du foyer (ex. Maison)";
"household_create_footer" = "Vos plats, étiquettes et semaines actuels deviennent le point de départ du foyer.";
"household_create_premium_footer" = "Créer un foyer est réservé au Premium. La personne invitée n'a rien à payer.";
"household_join_section" = "Rejoindre un foyer";
"household_join" = "Rejoindre avec le code";
"household_code_placeholder" = "Code d'invitation";
"household_join_footer" = "Demandez le code à la personne qui a créé le foyer. Il expire au bout de 7 jours.";
"household_join_choice_title" = "Et vos plats ?";
"household_join_choice_message" = "Vous pouvez emmener vos plats et vos semaines dans ce foyer, ou commencer avec ceux du foyer uniquement.";
"household_join_choice_merge" = "Emmener mes plats";
"household_join_choice_replace" = "Garder ceux du foyer";
"household_members_section" = "Membres";
"household_member_you" = "vous";
"household_member_unnamed" = "Sans nom";
"household_invite_section" = "Invitation";
"household_invite_footer" = "Partagez ce code avec les personnes qui planifieront avec vous.";
"household_invite_share" = "Rejoins mon foyer MealMood avec le code %@ — https://mealmood.app";
"household_invite_expires" = "Expire le %@";
"household_invite_regenerate" = "Générer un nouveau code";
"household_leave" = "Quitter le foyer";
"household_leave_confirm_title" = "Quitter le foyer ?";
"household_leave_confirm_message" = "Vous ne verrez plus et ne pourrez plus modifier les plats et les semaines du foyer sur cet appareil.";
"household_leave_confirm_confirm" = "Quitter";
"household_relaunch_needed" = "Fermez et rouvrez MealMood pour terminer le changement.";
"household_error_not_signed_in" = "Connectez-vous avant de créer un foyer ou d'en rejoindre un.";
"household_error_no_household" = "Cet appareil n'appartient à aucun foyer.";
"household_error_invalid_code" = "Ce code n'existe pas.";
"household_error_expired_code" = "Ce code a expiré. Demandez-en un nouveau.";
@@ -478,3 +478,40 @@
"week_schedule_warning_confirm" = "Rimuovi comunque";
"toast_week_schedule_updated" = "Settimana aggiornata";
"toast_week_schedule_reset" = "Settimana tornata alle impostazioni generali";
/* Hogar compartido (2.2) */
"household_title" = "Casa condivisa";
"household_section" = "Casa";
"household_settings_footer" = "Pianificate la settimana insieme, ognuno dal proprio account. Funziona anche da Android.";
"household_intro" = "Una casa permette a più persone di pianificare la stessa settimana, condividendo piatti e lista della spesa.";
"household_sign_in_footer" = "Per condividere serve accedere. I tuoi piatti restano tuoi: fuori dalla tua casa non li vede nessuno.";
"household_create_section" = "Creare una casa";
"household_create" = "Crea casa";
"household_name_placeholder" = "Nome della casa (es. Casa)";
"household_create_footer" = "I tuoi piatti, le etichette e le settimane attuali diventano il punto di partenza della casa.";
"household_create_premium_footer" = "Creare una casa è Premium. Chi ricevi l'invito non deve pagare.";
"household_join_section" = "Unirsi a una casa";
"household_join" = "Unisciti con il codice";
"household_code_placeholder" = "Codice d'invito";
"household_join_footer" = "Chiedi il codice a chi ha creato la casa. Scade dopo 7 giorni.";
"household_join_choice_title" = "E i tuoi piatti?";
"household_join_choice_message" = "Puoi portare in questa casa i piatti e le settimane che hai già, oppure iniziare solo con quelli della casa.";
"household_join_choice_merge" = "Porta i miei piatti";
"household_join_choice_replace" = "Solo quelli della casa";
"household_members_section" = "Membri";
"household_member_you" = "tu";
"household_member_unnamed" = "Senza nome";
"household_invite_section" = "Invito";
"household_invite_footer" = "Condividi questo codice con chi vuoi che pianifichi con te.";
"household_invite_share" = "Entra nella mia casa MealMood con il codice %@ — https://mealmood.app";
"household_invite_expires" = "Scade il %@";
"household_invite_regenerate" = "Genera un nuovo codice";
"household_leave" = "Esci dalla casa";
"household_leave_confirm_title" = "Uscire dalla casa?";
"household_leave_confirm_message" = "Su questo dispositivo non vedrai né modificherai più i piatti e le settimane della casa.";
"household_leave_confirm_confirm" = "Esci";
"household_relaunch_needed" = "Chiudi e riapri MealMood per completare il cambio.";
"household_error_not_signed_in" = "Accedi prima di creare una casa o di unirti a una.";
"household_error_no_household" = "Questo dispositivo non appartiene a nessuna casa.";
"household_error_invalid_code" = "Quel codice non esiste.";
"household_error_expired_code" = "Quel codice è scaduto. Chiedine uno nuovo.";
@@ -478,3 +478,40 @@
"week_schedule_warning_confirm" = "Remover mesmo assim";
"toast_week_schedule_updated" = "Semana atualizada";
"toast_week_schedule_reset" = "Semana de volta às configurações gerais";
/* Hogar compartido (2.2) */
"household_title" = "Casa compartilhada";
"household_section" = "Casa";
"household_settings_footer" = "Planejem a semana juntos, cada um pela sua conta. Funciona também no Android.";
"household_intro" = "Uma casa permite que várias pessoas planejem a mesma semana, com os pratos e a lista de compras em comum.";
"household_sign_in_footer" = "Para compartilhar é preciso entrar. Seus pratos continuam sendo seus: ninguém fora da sua casa os vê.";
"household_create_section" = "Criar uma casa";
"household_create" = "Criar casa";
"household_name_placeholder" = "Nome da casa (ex.: Casa)";
"household_create_footer" = "Seus pratos, etiquetas e semanas atuais viram o ponto de partida da casa.";
"household_create_premium_footer" = "Criar uma casa é Premium. Quem recebe o convite não precisa pagar.";
"household_join_section" = "Entrar em uma casa";
"household_join" = "Entrar com o código";
"household_code_placeholder" = "Código de convite";
"household_join_footer" = "Peça o código a quem criou a casa. Ele expira em 7 dias.";
"household_join_choice_title" = "E os seus pratos?";
"household_join_choice_message" = "Você pode levar para esta casa os pratos e semanas que já tem, ou começar só com os da casa.";
"household_join_choice_merge" = "Levar meus pratos";
"household_join_choice_replace" = "Usar só os da casa";
"household_members_section" = "Integrantes";
"household_member_you" = "você";
"household_member_unnamed" = "Sem nome";
"household_invite_section" = "Convite";
"household_invite_footer" = "Compartilhe este código com quem vai planejar com você.";
"household_invite_share" = "Entre na minha casa do MealMood com o código %@ — https://mealmood.app";
"household_invite_expires" = "Expira em %@";
"household_invite_regenerate" = "Gerar um código novo";
"household_leave" = "Sair da casa";
"household_leave_confirm_title" = "Sair da casa?";
"household_leave_confirm_message" = "Você deixará de ver e editar os pratos e as semanas da casa neste dispositivo.";
"household_leave_confirm_confirm" = "Sair";
"household_relaunch_needed" = "Feche e abra o MealMood de novo para concluir a mudança.";
"household_error_not_signed_in" = "Entre na sua conta antes de criar ou entrar em uma casa.";
"household_error_no_household" = "Este dispositivo não pertence a nenhuma casa.";
"household_error_invalid_code" = "Esse código não existe.";
"household_error_expired_code" = "Esse código expirou. Peça um novo.";