313 lines
9.0 KiB
Dart
313 lines
9.0 KiB
Dart
import 'package:appwrite/appwrite.dart';
|
||
import 'package:flutter/material.dart';
|
||
|
||
import '../../appwrite_config.dart';
|
||
import '../../theme/app_theme.dart';
|
||
|
||
/// Screen 1: Login & Registrierung (E-Mail / Passwort) über Appwrite.
|
||
class AuthScreen extends StatefulWidget {
|
||
const AuthScreen({super.key, required this.onLoggedIn});
|
||
|
||
final VoidCallback onLoggedIn;
|
||
|
||
@override
|
||
State<AuthScreen> createState() => _AuthScreenState();
|
||
}
|
||
|
||
class _AuthScreenState extends State<AuthScreen>
|
||
with SingleTickerProviderStateMixin {
|
||
late final TabController _tabController;
|
||
final _account = Account(appwriteClient);
|
||
|
||
final _loginEmail = TextEditingController();
|
||
final _loginPassword = TextEditingController();
|
||
|
||
final _regName = TextEditingController();
|
||
final _regEmail = TextEditingController();
|
||
final _regPassword = TextEditingController();
|
||
|
||
bool _loading = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_tabController = TabController(length: 2, vsync: this);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_tabController.dispose();
|
||
_loginEmail.dispose();
|
||
_loginPassword.dispose();
|
||
_regName.dispose();
|
||
_regEmail.dispose();
|
||
_regPassword.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _snack(String text) {
|
||
if (!mounted) return;
|
||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));
|
||
}
|
||
|
||
String _authMessage(AppwriteException e) {
|
||
final t = e.type ?? '';
|
||
final m = (e.message ?? '').toLowerCase();
|
||
if (t == 'user_invalid_credentials' ||
|
||
m.contains('invalid credentials') ||
|
||
m.contains('wrong password')) {
|
||
return 'E-Mail oder Passwort ist falsch.';
|
||
}
|
||
if (t == 'user_already_exists' || m.contains('already exists')) {
|
||
return 'Diese E-Mail ist bereits registriert.';
|
||
}
|
||
if (m.contains('password') && m.contains('short')) {
|
||
return 'Passwort ist zu schwach (mindestens 8 Zeichen in Appwrite).';
|
||
}
|
||
if (t == 'general_argument_invalid' && m.contains('email')) {
|
||
return 'Ungültige E-Mail-Adresse.';
|
||
}
|
||
if (e.code == 0 && m.contains('network')) {
|
||
return 'Netzwerkfehler. Internet prüfen.';
|
||
}
|
||
return e.message?.isNotEmpty == true ? e.message! : 'Fehler: $t';
|
||
}
|
||
|
||
Future<void> _login() async {
|
||
final email = _loginEmail.text.trim();
|
||
final password = _loginPassword.text;
|
||
if (email.isEmpty || password.isEmpty) {
|
||
_snack('E-Mail und Passwort eingeben.');
|
||
return;
|
||
}
|
||
setState(() => _loading = true);
|
||
try {
|
||
await _account.createEmailPasswordSession(
|
||
email: email,
|
||
password: password,
|
||
);
|
||
if (mounted) widget.onLoggedIn();
|
||
} on AppwriteException catch (e) {
|
||
_snack(_authMessage(e));
|
||
} catch (e) {
|
||
_snack('Unerwarteter Fehler: $e');
|
||
} finally {
|
||
if (mounted) setState(() => _loading = false);
|
||
}
|
||
}
|
||
|
||
Future<void> _register() async {
|
||
final email = _regEmail.text.trim();
|
||
final password = _regPassword.text;
|
||
final name = _regName.text.trim();
|
||
if (email.isEmpty || password.isEmpty) {
|
||
_snack('E-Mail und Passwort eingeben.');
|
||
return;
|
||
}
|
||
if (password.length < 8) {
|
||
_snack('Passwort mindestens 8 Zeichen (Appwrite-Standard).');
|
||
return;
|
||
}
|
||
setState(() => _loading = true);
|
||
try {
|
||
await _account.create(
|
||
userId: ID.unique(),
|
||
email: email,
|
||
password: password,
|
||
name: name.isEmpty ? null : name,
|
||
);
|
||
await _account.createEmailPasswordSession(
|
||
email: email,
|
||
password: password,
|
||
);
|
||
if (name.isNotEmpty) {
|
||
try {
|
||
await _account.updateName(name: name);
|
||
} catch (_) {}
|
||
}
|
||
if (mounted) widget.onLoggedIn();
|
||
} on AppwriteException catch (e) {
|
||
_snack(_authMessage(e));
|
||
} catch (e) {
|
||
_snack('Unerwarteter Fehler: $e');
|
||
} finally {
|
||
if (mounted) setState(() => _loading = false);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final scheme = Theme.of(context).colorScheme;
|
||
|
||
return Scaffold(
|
||
backgroundColor: AppTheme.background,
|
||
body: SafeArea(
|
||
child: ListView(
|
||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||
children: [
|
||
const SizedBox(height: 24),
|
||
Icon(Icons.handyman_rounded, size: 48, color: scheme.primary),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'HandwerkPro',
|
||
textAlign: TextAlign.center,
|
||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'Aufträge, Fotos, Unterschrift, PDF – alles in einer App. '
|
||
'Anmelden oder registrieren (Appwrite).',
|
||
textAlign: TextAlign.center,
|
||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||
color: const Color(0xFFB0B0B0),
|
||
),
|
||
),
|
||
const SizedBox(height: 32),
|
||
TabBar(
|
||
controller: _tabController,
|
||
tabs: const [
|
||
Tab(text: 'Anmelden'),
|
||
Tab(text: 'Registrieren'),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
SizedBox(
|
||
height: 320,
|
||
child: TabBarView(
|
||
controller: _tabController,
|
||
children: [
|
||
_LoginForm(
|
||
email: _loginEmail,
|
||
password: _loginPassword,
|
||
loading: _loading,
|
||
onSubmit: _login,
|
||
),
|
||
_RegisterForm(
|
||
name: _regName,
|
||
email: _regEmail,
|
||
password: _regPassword,
|
||
loading: _loading,
|
||
onSubmit: _register,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _LoginForm extends StatelessWidget {
|
||
const _LoginForm({
|
||
required this.email,
|
||
required this.password,
|
||
required this.loading,
|
||
required this.onSubmit,
|
||
});
|
||
|
||
final TextEditingController email;
|
||
final TextEditingController password;
|
||
final bool loading;
|
||
final VoidCallback onSubmit;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
TextField(
|
||
controller: email,
|
||
keyboardType: TextInputType.emailAddress,
|
||
autocorrect: false,
|
||
autofillHints: const [AutofillHints.email],
|
||
decoration: const InputDecoration(labelText: 'E-Mail'),
|
||
),
|
||
const SizedBox(height: 12),
|
||
TextField(
|
||
controller: password,
|
||
obscureText: true,
|
||
autofillHints: const [AutofillHints.password],
|
||
decoration: const InputDecoration(labelText: 'Passwort'),
|
||
onSubmitted: (_) => onSubmit(),
|
||
),
|
||
const Spacer(),
|
||
FilledButton(
|
||
onPressed: loading ? null : onSubmit,
|
||
child: loading
|
||
? const SizedBox(
|
||
height: 22,
|
||
width: 22,
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
)
|
||
: const Text('Anmelden'),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _RegisterForm extends StatelessWidget {
|
||
const _RegisterForm({
|
||
required this.name,
|
||
required this.email,
|
||
required this.password,
|
||
required this.loading,
|
||
required this.onSubmit,
|
||
});
|
||
|
||
final TextEditingController name;
|
||
final TextEditingController email;
|
||
final TextEditingController password;
|
||
final bool loading;
|
||
final VoidCallback onSubmit;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
TextField(
|
||
controller: name,
|
||
textCapitalization: TextCapitalization.words,
|
||
decoration: const InputDecoration(
|
||
labelText: 'Firmen- oder Anzeigename',
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
TextField(
|
||
controller: email,
|
||
keyboardType: TextInputType.emailAddress,
|
||
autocorrect: false,
|
||
autofillHints: const [AutofillHints.email],
|
||
decoration: const InputDecoration(labelText: 'E-Mail'),
|
||
),
|
||
const SizedBox(height: 12),
|
||
TextField(
|
||
controller: password,
|
||
obscureText: true,
|
||
autofillHints: const [AutofillHints.newPassword],
|
||
decoration: const InputDecoration(
|
||
labelText: 'Passwort (min. 8 Zeichen)',
|
||
),
|
||
),
|
||
const Spacer(),
|
||
FilledButton(
|
||
onPressed: loading ? null : onSubmit,
|
||
child: loading
|
||
? const SizedBox(
|
||
height: 22,
|
||
width: 22,
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
)
|
||
: const Text('Konto erstellen'),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|