From bc0588f3589fa4226cc00634b5780e0a98cfd32b Mon Sep 17 00:00:00 2001 From: cschlaefke Date: Thu, 23 Jul 2026 21:54:32 +0200 Subject: [PATCH] V2.x: Anmelden mit Google (iOS + Android) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wie Apple über den eingebauten GoogleAuthProvider (Browser-Flow über den Firebase-Auth-Handler, kein Zusatz-Paket). Gemeinsamer Provider-Handler im Login-Screen, l10n-Strings, iOS-URL-Scheme (Encoded App ID) für den Rückweg aus dem Browser, Doku Schritt 8. Der reaktive Haushalts-Bootstrap deckt den ersten Google-Login automatisch ab. Offen: Google-Provider in der Firebase-Konsole aktivieren + Gerätetest. Co-Authored-By: Claude Fable 5 --- docs/firebase-einrichtung.md | 18 ++++++- docs/handoff.md | 8 +++- ios/Runner/Info.plist | 12 +++++ lib/features/auth/data/auth_repository.dart | 8 ++++ .../auth/presentation/login_screen.dart | 48 +++++++++++++++---- lib/l10n/app_de.arb | 2 + lib/l10n/generated/app_localizations.dart | 12 +++++ lib/l10n/generated/app_localizations_de.dart | 7 +++ 8 files changed, 101 insertions(+), 14 deletions(-) diff --git a/docs/firebase-einrichtung.md b/docs/firebase-einrichtung.md index c592868..2a0bb89 100644 --- a/docs/firebase-einrichtung.md +++ b/docs/firebase-einrichtung.md @@ -167,9 +167,23 @@ Danach neu bauen (`flutter run`) und den Apple-Button auf iPhone **und** Android > **Hinweis:** Apple liefert Name/E-Mail nur beim allerersten Login an eine App. Zum erneuten Testen des Erst-Login-Flows die App unter → *Anmelden mit Apple* → für „LeafItToMe" die Freigabe entfernen; beim nächsten Login fragt Apple wieder nach den Daten. -## Schritt 8 und folgende (kommen mit den nächsten Blöcken) +## Schritt 8: Anmelden mit Google (iOS + Android) + +**So funktioniert es:** Der Login-Screen hat unter dem Apple-Button einen Button „Mit Google anmelden". Auf beiden Plattformen öffnet sich ein Browser-Fenster, das über den Firebase-Auth-Handler zu Google und wieder zurück führt — über den in `firebase_auth` eingebauten `GoogleAuthProvider`, genau wie beim Apple-Login auf Android (kein Zusatz-Paket, kein `google_sign_in`). Beim ersten Google-Login legt der reaktive Haushalts-Bootstrap automatisch Profil + Haushalt an. Der Code liegt in `AuthRepository.signInWithGoogle()`. + +**Am Code ist nichts mehr zu tun** — im Repo sind Button, Fehlerbehandlung und das iOS-URL-Scheme (Encoded App ID in `ios/Runner/Info.plist`, der Rückweg aus dem Browser in die App) schon eingerichtet. Es fehlt nur **ein** Konsolen-Schritt: + +1. **Firebase-Konsole:** *Authentication → Sign-in method* → **Google** aktivieren. Dabei den öffentlichen App-Namen prüfen und als *Support-E-Mail* deine Adresse auswählen → Speichern. (Firebase legt die nötigen OAuth-Clients selbst an.) + +Danach neu bauen (`flutter run`) und den Google-Button auf iPhone **und** Android testen — der erste Login mit einem Google-Konto muss kurz „Dein Haushalt wird eingerichtet …" zeigen und danach normal funktionieren. + +> **Hinweise:** +> - Anders als bei Apple ist **kein** Entwickler-Portal, kein Schlüssel und keine Services-ID nötig — der eine Konsolen-Schalter genügt. +> - Ein **SHA-1-Fingerabdruck** (den Anleitungen für das `google_sign_in`-Paket verlangen) ist für unseren Browser-Flow **nicht** erforderlich. +> - Die `GoogleService-Info.plist`/`google-services.json` müssen nicht neu heruntergeladen werden — der Flow läuft über den Auth-Handler im Web, nicht über das native Google-SDK. + +## Schritt 9 und folgende (kommen mit den nächsten Blöcken) -- **Google-Login** — zusätzlich zu E-Mail/Passwort und Apple. - **App Check** — optionale Härtung vor dem App-Store-Release: bestätigt per Play Integrity (Android) / App Attest (iOS), dass Anfragen aus der echten App kommen. Bis dahin ist die Log-Warnung „No AppCheckProvider installed" harmlos und kann ignoriert werden — die Zugriffskontrolle leisten Login + Security Rules. ## Begriffe kurz erklärt diff --git a/docs/handoff.md b/docs/handoff.md index f21d271..f71b4a6 100644 --- a/docs/handoff.md +++ b/docs/handoff.md @@ -14,7 +14,7 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si - **Claude-Memory:** Projektplan, Nutzerprofil und Backlog liegen im persistenten Memory-Verzeichnis dieser Claude-Code-Installation (werden automatisch geladen) - **Firebase-Projekt:** `leaf-it-to-me-app` (Blaze aktiv, Budget-Alarm eingerichtet; Firebase CLI ist eingeloggt) - **Functions:** `functions/` (TypeScript, Node 22, Region europe-west3): `identifyPlant` (PlantNet + Claude, Secrets `PLANTNET_API_KEY`/`ANTHROPIC_API_KEY` im Secret Manager), `sendDailyReminders` (alle 15 Min, sammelt über alle Haushalte eines Nutzers), `joinHousehold` (Einladungs-Einlösung), `leaveHousehold` (Haushalt verlassen), `removeMember` (Mitglied entfernen, nur Besitzer) -- **Auth:** E-Mail/Passwort + **Anmelden mit Apple** (iOS nativ, Android Web-Flow über Firebase-Handler) — Code in `lib/features/auth/data/auth_repository.dart`, Login-UI in `.../presentation/login_screen.dart`. Haushalts-Bootstrap (Profil + eigener Haushalt beim ersten Login) im gemeinsamen Helper `_bootstrapHouseholdIfNeeded`. +- **Auth:** E-Mail/Passwort + **Anmelden mit Apple** (iOS nativ, Android Web-Flow über Firebase-Handler) + **Anmelden mit Google** (beide Plattformen Web-Flow, eingebauter `GoogleAuthProvider`) — Code in `lib/features/auth/data/auth_repository.dart`, Login-UI in `.../presentation/login_screen.dart`. Haushalts-Bootstrap (Profil + eigener Haushalt beim ersten Login) läuft **reaktiv** über `householdBootstrapProvider` + `BootstrapGate` (Maßstab: `householdId` im users-Doc). ## Verifizierter Stand @@ -27,6 +27,10 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si ## Offen / Nächstes +### 🟡 Google-Login: implementiert, wartet auf Konsolen-Schalter + Gerätetest + +**2026-07-23 implementiert** (`AuthRepository.signInWithGoogle` via eingebautem `GoogleAuthProvider`, Google-Button im Login mit gemeinsamem Provider-Handler `_signInWithProvider`, l10n-Strings, iOS-URL-Scheme/Encoded App ID in `Info.plist`, Doku `firebase-einrichtung.md` Schritt 8). Analyzer + alle 10 Tests grün. **Nutzer muss noch:** In der Firebase-Konsole *Authentication → Sign-in method → Google* aktivieren (Support-E-Mail wählen — der einzige Konfigurationsschritt), dann auf iPhone und Android testen; der erste Google-Login muss kurz den Einrichtungs-Bildschirm zeigen (reaktiver Bootstrap deckt Google automatisch ab). + ### ✅ Apple-Login-Bug behoben und vom Nutzer bestätigt (2026-07-23) **Der Bug in Kurzform:** Mit einem neuen Apple-Account schlug jedes Speichern fehl. Zwei Ursachen-Schichten: @@ -49,7 +53,7 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si ### Weitere Kandidaten (nach dem Bug) 1. **V3:** Krankheits-Diagnose per Foto, Stellplatz-Bewertung (Standort-Analyse pro Stellplatz + Eignungs-Sterne pro Pflanze — Zwei-Ebenen-Konzept siehe Memory), Umtopf-Erinnerungen. -2. **Backlog:** Google-Login (nach Apple, `firebase-einrichtung.md` Schritt 8); Button „KI-Zweitmeinung" bei unbefriedigendem PlantNet-Ergebnis; Push-Anzeige auch bei App im Vordergrund; App Check vor App-Store-Release; Englisch (`app_en.arb`). +2. **Backlog:** Button „KI-Zweitmeinung" bei unbefriedigendem PlantNet-Ergebnis; Push-Anzeige auch bei App im Vordergrund; App Check vor App-Store-Release (`firebase-einrichtung.md` Schritt 9); Englisch (`app_en.arb`). ## Stolperfallen (teuer erkauft, nicht erneut zahlen) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e89e550..c635b57 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -24,6 +24,18 @@ ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) + CFBundleURLTypes + + + CFBundleURLSchemes + + + app-1-724282636354-ios-7c2855f20b2ba5ba4195db + + + LSRequiresIPhoneOS NSCameraUsageDescription diff --git a/lib/features/auth/data/auth_repository.dart b/lib/features/auth/data/auth_repository.dart index 51b2515..f9985cf 100644 --- a/lib/features/auth/data/auth_repository.dart +++ b/lib/features/auth/data/auth_repository.dart @@ -40,6 +40,14 @@ class AuthRepository { await _auth.signInWithProvider(provider); } + /// Anmeldung mit Google — wie bei Apple über den eingebauten + /// [GoogleAuthProvider] (Browser-Flow über den Firebase-Auth-Handler, + /// kein Zusatz-Paket). Der Haushalts-Bootstrap läuft auch hier reaktiv + /// über den householdBootstrapProvider. + Future signInWithGoogle() async { + await _auth.signInWithProvider(GoogleAuthProvider()); + } + Future? _bootstrapInFlight; /// Legt Profil-Dokument und einen eigenen Haushalt an, falls für [uid] diff --git a/lib/features/auth/presentation/login_screen.dart b/lib/features/auth/presentation/login_screen.dart index b4a60ea..eef05da 100644 --- a/lib/features/auth/presentation/login_screen.dart +++ b/lib/features/auth/presentation/login_screen.dart @@ -67,9 +67,9 @@ class _LoginScreenState extends ConsumerState { } } - /// Codes, mit denen Firebase einen vom Nutzer abgebrochenen Apple-Dialog - /// meldet — kein Fehler, deshalb ohne rote Meldung. - static const _appleCancelCodes = { + /// Codes, mit denen Firebase einen vom Nutzer abgebrochenen + /// Apple-/Google-Dialog meldet — kein Fehler, deshalb ohne rote Meldung. + static const _providerCancelCodes = { 'canceled', 'cancelled', 'web-context-canceled', @@ -77,21 +77,25 @@ class _LoginScreenState extends ConsumerState { 'user-cancelled', }; - Future _signInWithApple() async { - final l10n = AppLocalizations.of(context); + /// Gemeinsamer Ablauf für Drittanbieter-Logins (Apple, Google): + /// [signIn] ausführen, Abbrüche schlucken, Fehler als [errorMessage] zeigen. + Future _signInWithProvider( + Future Function() signIn, + String errorMessage, + ) async { setState(() { _busy = true; _errorText = null; }); try { - await ref.read(authRepositoryProvider).signInWithApple(); + await signIn(); // Weiterleitung übernimmt der Router (redirect bei Auth-Änderung). } on FirebaseAuthException catch (e) { - if (!_appleCancelCodes.contains(e.code)) { - setState(() => _errorText = l10n.authErrorAppleFailed); + if (!_providerCancelCodes.contains(e.code)) { + setState(() => _errorText = errorMessage); } } catch (_) { - setState(() => _errorText = l10n.authErrorAppleFailed); + setState(() => _errorText = errorMessage); } finally { if (mounted) setState(() => _busy = false); } @@ -208,10 +212,34 @@ class _LoginScreenState extends ConsumerState { ), const SizedBox(height: 8), OutlinedButton.icon( - onPressed: _busy ? null : _signInWithApple, + onPressed: _busy + ? null + : () => _signInWithProvider( + ref.read(authRepositoryProvider).signInWithApple, + l10n.authErrorAppleFailed, + ), icon: const Icon(Icons.apple), label: Text(l10n.signInWithApple), ), + const SizedBox(height: 8), + OutlinedButton.icon( + onPressed: _busy + ? null + : () => _signInWithProvider( + ref.read(authRepositoryProvider).signInWithGoogle, + l10n.authErrorGoogleFailed, + ), + // Material bringt kein Google-Logo mit — ein + // schlichtes „G“ in Button-Farbe reicht hier. + icon: const Text( + 'G', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + label: Text(l10n.signInWithGoogle), + ), ], ), ), diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 6dc1b1e..6c00781 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -206,7 +206,9 @@ "switchToSignIn": "Du hast schon ein Konto? Anmelden", "orDivider": "oder", "signInWithApple": "Mit Apple anmelden", + "signInWithGoogle": "Mit Google anmelden", "authErrorAppleFailed": "Die Anmeldung mit Apple hat nicht geklappt. Bitte versuche es erneut.", + "authErrorGoogleFailed": "Die Anmeldung mit Google hat nicht geklappt. Bitte versuche es erneut.", "authErrorInvalidCredential": "E-Mail-Adresse oder Passwort ist falsch.", "authErrorEmailInUse": "Mit dieser E-Mail-Adresse gibt es bereits ein Konto.", "authErrorWeakPassword": "Das Passwort muss mindestens 6 Zeichen lang sein.", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index f075886..3d2dafd 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -730,12 +730,24 @@ abstract class AppLocalizations { /// **'Mit Apple anmelden'** String get signInWithApple; + /// No description provided for @signInWithGoogle. + /// + /// In de, this message translates to: + /// **'Mit Google anmelden'** + String get signInWithGoogle; + /// No description provided for @authErrorAppleFailed. /// /// In de, this message translates to: /// **'Die Anmeldung mit Apple hat nicht geklappt. Bitte versuche es erneut.'** String get authErrorAppleFailed; + /// No description provided for @authErrorGoogleFailed. + /// + /// In de, this message translates to: + /// **'Die Anmeldung mit Google hat nicht geklappt. Bitte versuche es erneut.'** + String get authErrorGoogleFailed; + /// No description provided for @authErrorInvalidCredential. /// /// In de, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 869e1ad..a7186f7 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -390,10 +390,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String get signInWithApple => 'Mit Apple anmelden'; + @override + String get signInWithGoogle => 'Mit Google anmelden'; + @override String get authErrorAppleFailed => 'Die Anmeldung mit Apple hat nicht geklappt. Bitte versuche es erneut.'; + @override + String get authErrorGoogleFailed => + 'Die Anmeldung mit Google hat nicht geklappt. Bitte versuche es erneut.'; + @override String get authErrorInvalidCredential => 'E-Mail-Adresse oder Passwort ist falsch.';