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:
parent
51a304e986
commit
bc0588f358
8 changed files with 101 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
|
|
|
|||
Loading…
Reference in a new issue