V2.x: Anmelden mit Apple (iOS + Android)
- AuthRepository.signInWithApple() via eingebautem AppleAuthProvider (nativ auf iOS, Web-Flow über Firebase-Handler auf Android, ohne Zusatzpaket) - Haushalts-Bootstrap in gemeinsamen Helper _bootstrapHouseholdIfNeeded ausgelagert; beim ersten Apple-Login wird Profil + Haushalt idempotent angelegt - LoginScreen: "oder"-Trenner + "Mit Apple anmelden"-Button, Abbruch ohne Fehlermeldung - l10n: signInWithApple, orDivider, authErrorAppleFailed - iOS: Entitlement com.apple.developer.applesignin ergänzt - Doku: firebase-einrichtung.md Schritt 7 (Apple-Login) mit Portal-/Console-Anleitung Noch offen: Apple-Portal (Services-ID, Sign-in-Key), Firebase-Console-Provider, Xcode-Capability — danach Ende-zu-Ende auf iOS+Android testen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
da12e3e5ab
commit
798912cb03
7 changed files with 139 additions and 9 deletions
|
|
@ -143,9 +143,33 @@ Danach einmal neu bauen (`flutter run`) — die Erinnerungen landen dann auch au
|
|||
|
||||
> **Stolperfalle (teuer erkauft):** „Push Notifications" fehlte in Xcode unter „+ Capability", obwohl „Background Modes" da war. Ursache: Es war noch das alte **kostenlose „Personal Team"** als Signing-Team gewählt — Gratis-Teams dürfen kein Push, deshalb blendet Xcode den Eintrag komplett aus. Fix: In *Xcode → Settings → Accounts* die Apple-ID mit der bezahlten Mitgliedschaft hinzufügen und in *Signing & Capabilities* das Team **ohne** den Zusatz `(Personal Team)` wählen. Danach erscheint „Push Notifications" in der Liste.
|
||||
|
||||
## Schritt 7 und folgende (kommen mit den nächsten Blöcken)
|
||||
## Schritt 7: Anmelden mit Apple (iOS + Android)
|
||||
|
||||
- **Google-/Apple-Login** — zusätzlich zu E-Mail/Passwort (Apple-Login braucht ebenfalls den Developer Account).
|
||||
**So funktioniert es:** Der Login-Screen hat neben E-Mail/Passwort einen Button „Mit Apple anmelden". Auf iOS öffnet sich der native Apple-Dialog, auf Android ein Browser-Fenster, das über den Firebase-Auth-Handler zu Apple und wieder zurück führt — beides über den in `firebase_auth` eingebauten `AppleAuthProvider` (kein Zusatz-Paket). Beim **ersten** Apple-Login legt die App automatisch Profil-Dokument und eigenen Haushalt an (wie bei der normalen Registrierung); bei „E-Mail verbergen“ wird die anonyme Apple-Relay-Adresse gespeichert. Der Code liegt in `AuthRepository.signInWithApple()`.
|
||||
|
||||
**Am Code ist nichts mehr zu tun** — die folgende Konfiguration in Apple-Portal, Firebase-Konsole und Xcode macht den Button funktionsfähig:
|
||||
|
||||
1. **App-ID-Capability (iOS):** In Xcode (`ios/Runner.xcworkspace`) → *Runner* → *Signing & Capabilities* → „+ Capability" → **Sign in with Apple** hinzufügen. Das Entitlement `com.apple.developer.applesignin` ist im Repo schon gesetzt; der Xcode-Schritt aktiviert die Capability zusätzlich an der App-ID `dev.leafittome.app` und frischt das Provisioning-Profil auf (mit „Automatically manage signing").
|
||||
|
||||
2. **Services-ID (für Android/Web):** <https://developer.apple.com> → *Identifiers* → „+" → **Services IDs** → z. B. Beschreibung „LeafItToMe Sign-In", Identifier `dev.leafittome.signin` (frei wählbar, aber ≠ Bundle-ID). Nach dem Anlegen die Services-ID öffnen → **Sign In with Apple** aktivieren → **Configure**:
|
||||
- *Primary App ID:* `dev.leafittome.app`
|
||||
- *Domains and Subdomains:* `leaf-it-to-me-app.firebaseapp.com`
|
||||
- *Return URLs:* `https://leaf-it-to-me-app.firebaseapp.com/__/auth/handler`
|
||||
|
||||
3. **Sign-in-Schlüssel (.p8):** *Keys* → „+" → **Sign in with Apple** ankreuzen → Configure → Primary App ID `dev.leafittome.app` → Register → **.p8 herunterladen** (nur einmal möglich!) und **Key ID** notieren. (Eigener Schlüssel, nicht der APNs-Schlüssel aus Schritt 6.)
|
||||
|
||||
4. **Firebase-Konsole:** *Authentication → Sign-in method* → **Apple** aktivieren. Für den Android/Web-Flow den Abschnitt **„OAuth-Codeflow konfigurieren"** ausfüllen:
|
||||
- *Services ID:* `dev.leafittome.signin`
|
||||
- *Apple Team ID:* (steht oben rechts im Developer-Account)
|
||||
- *Key ID* + Inhalt der **.p8-Datei**
|
||||
|
||||
Danach neu bauen (`flutter run`) und den Apple-Button auf iPhone **und** Android-Gerät testen. Erststart legt einen frischen Haushalt an — der wird in `households` sichtbar.
|
||||
|
||||
> **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)
|
||||
|
||||
- **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
|
||||
|
|
|
|||
|
|
@ -4,5 +4,9 @@
|
|||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -15,18 +15,39 @@ class AuthRepository {
|
|||
await _auth.signInWithEmailAndPassword(email: email, password: password);
|
||||
}
|
||||
|
||||
/// Registrierung: legt Nutzer, persönliches Profil-Dokument und einen
|
||||
/// neuen Haushalt in einem Rutsch an. Der Nutzer ist automatisch Mitglied
|
||||
/// seines eigenen Haushalts (V2 fügt per Einladung weitere hinzu).
|
||||
/// Registrierung: legt den Auth-Nutzer an und danach Profil + Haushalt.
|
||||
Future<void> signUp({required String email, required String password}) async {
|
||||
final credential = await _auth.createUserWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
final uid = credential.user!.uid;
|
||||
await _bootstrapHouseholdIfNeeded(credential.user!.uid, email);
|
||||
}
|
||||
|
||||
final householdRef = _firestore.collection('households').doc();
|
||||
/// Anmeldung mit Apple. Nativer Dialog auf iOS, Web-Flow (über den
|
||||
/// Firebase-Auth-Handler) auf Android — beides über den eingebauten
|
||||
/// [AppleAuthProvider], daher ohne Zusatz-Paket. Beim **ersten** Login
|
||||
/// existiert noch kein Firestore-Profil, deshalb hängt der Haushalts-
|
||||
/// Bootstrap hier dran (Apple legt nur den Auth-Nutzer an).
|
||||
Future<void> signInWithApple() async {
|
||||
final provider = AppleAuthProvider()
|
||||
..addScope('email')
|
||||
..addScope('name');
|
||||
final credential = await _auth.signInWithProvider(provider);
|
||||
final user = credential.user!;
|
||||
await _bootstrapHouseholdIfNeeded(user.uid, user.email);
|
||||
}
|
||||
|
||||
/// Legt Profil-Dokument und einen eigenen Haushalt an, falls für [uid]
|
||||
/// noch keins existiert. Idempotent — mehrfaches Anmelden mit Apple oder
|
||||
/// eine erneute Registrierung überschreibt einen bestehenden Haushalt nie.
|
||||
/// [email] kann bei „E-Mail verbergen“ die Apple-Relay-Adresse sein.
|
||||
Future<void> _bootstrapHouseholdIfNeeded(String uid, String? email) async {
|
||||
final userRef = _firestore.collection('users').doc(uid);
|
||||
if ((await userRef.get()).exists) return;
|
||||
|
||||
final safeEmail = email ?? '';
|
||||
final householdRef = _firestore.collection('households').doc();
|
||||
final batch = _firestore.batch();
|
||||
batch.set(householdRef, {
|
||||
'name': 'Mein Haushalt',
|
||||
|
|
@ -35,14 +56,14 @@ class AuthRepository {
|
|||
'members': {
|
||||
uid: {
|
||||
'role': 'member',
|
||||
'email': email,
|
||||
'email': safeEmail,
|
||||
'joinedAt': FieldValue.serverTimestamp(),
|
||||
},
|
||||
},
|
||||
'createdAt': FieldValue.serverTimestamp(),
|
||||
});
|
||||
batch.set(userRef, {
|
||||
'email': email,
|
||||
'email': safeEmail,
|
||||
'householdId': householdRef.id,
|
||||
'createdAt': FieldValue.serverTimestamp(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,6 +67,36 @@ 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 = {
|
||||
'canceled',
|
||||
'cancelled',
|
||||
'web-context-canceled',
|
||||
'web-context-cancelled',
|
||||
'user-cancelled',
|
||||
};
|
||||
|
||||
Future<void> _signInWithApple() async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
setState(() {
|
||||
_busy = true;
|
||||
_errorText = null;
|
||||
});
|
||||
try {
|
||||
await ref.read(authRepositoryProvider).signInWithApple();
|
||||
// Weiterleitung übernimmt der Router (redirect bei Auth-Änderung).
|
||||
} on FirebaseAuthException catch (e) {
|
||||
if (!_appleCancelCodes.contains(e.code)) {
|
||||
setState(() => _errorText = l10n.authErrorAppleFailed);
|
||||
}
|
||||
} catch (_) {
|
||||
setState(() => _errorText = l10n.authErrorAppleFailed);
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
|
@ -162,6 +192,26 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
? l10n.switchToSignIn
|
||||
: l10n.switchToSignUp),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(child: Divider()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
l10n.orDivider,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
const Expanded(child: Divider()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _busy ? null : _signInWithApple,
|
||||
icon: const Icon(Icons.apple),
|
||||
label: Text(l10n.signInWithApple),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -204,6 +204,9 @@
|
|||
"signUpButton": "Konto erstellen",
|
||||
"switchToSignUp": "Neu hier? Konto erstellen",
|
||||
"switchToSignIn": "Du hast schon ein Konto? Anmelden",
|
||||
"orDivider": "oder",
|
||||
"signInWithApple": "Mit Apple anmelden",
|
||||
"authErrorAppleFailed": "Die Anmeldung mit Apple 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.",
|
||||
|
|
|
|||
|
|
@ -718,6 +718,24 @@ abstract class AppLocalizations {
|
|||
/// **'Du hast schon ein Konto? Anmelden'**
|
||||
String get switchToSignIn;
|
||||
|
||||
/// No description provided for @orDivider.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'oder'**
|
||||
String get orDivider;
|
||||
|
||||
/// No description provided for @signInWithApple.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Mit Apple anmelden'**
|
||||
String get signInWithApple;
|
||||
|
||||
/// 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 @authErrorInvalidCredential.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
|
|
|
|||
|
|
@ -384,6 +384,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get switchToSignIn => 'Du hast schon ein Konto? Anmelden';
|
||||
|
||||
@override
|
||||
String get orDivider => 'oder';
|
||||
|
||||
@override
|
||||
String get signInWithApple => 'Mit Apple anmelden';
|
||||
|
||||
@override
|
||||
String get authErrorAppleFailed =>
|
||||
'Die Anmeldung mit Apple hat nicht geklappt. Bitte versuche es erneut.';
|
||||
|
||||
@override
|
||||
String get authErrorInvalidCredential =>
|
||||
'E-Mail-Adresse oder Passwort ist falsch.';
|
||||
|
|
|
|||
Loading…
Reference in a new issue