V2.x: Anmelden mit Google (iOS + Android)

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 <noreply@anthropic.com>
This commit is contained in:
cschlaefke 2026-07-23 21:54:32 +02:00
parent 51a304e986
commit bc0588f358
8 changed files with 101 additions and 14 deletions

View file

@ -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 <https://appleid.apple.com>*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

View file

@ -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)

View file

@ -24,6 +24,18 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<!-- Encoded App ID (GOOGLE_APP_ID mit Bindestrichen): Rückweg
des Browser-Login-Flows (signInWithProvider, z. B. Google)
zurück in die App. -->
<string>app-1-724282636354-ios-7c2855f20b2ba5ba4195db</string>
</array>
</dict>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>

View file

@ -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<void> signInWithGoogle() async {
await _auth.signInWithProvider(GoogleAuthProvider());
}
Future<void>? _bootstrapInFlight;
/// Legt Profil-Dokument und einen eigenen Haushalt an, falls für [uid]

View file

@ -67,9 +67,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
}
}
/// 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<LoginScreen> {
'user-cancelled',
};
Future<void> _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<void> _signInWithProvider(
Future<void> 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<LoginScreen> {
),
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),
),
],
),
),

View file

@ -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.",

View file

@ -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:

View file

@ -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.';