204 lines
6.7 KiB
Dart
204 lines
6.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:login_vault_app/auth_service.dart';
|
|
import 'package:login_vault_app/dashboard_screen.dart';
|
|
import 'package:login_vault_app/theme_provider.dart';
|
|
|
|
/// First screen: register (if not yet), or re-authenticate (if registered).
|
|
/// If a valid session exists, skip straight to the Dashboard.
|
|
class AuthScreen extends StatefulWidget {
|
|
final AuthService auth;
|
|
const AuthScreen({super.key, required this.auth});
|
|
|
|
@override
|
|
State<AuthScreen> createState() => _AuthScreenState();
|
|
}
|
|
|
|
class _AuthScreenState extends State<AuthScreen> {
|
|
final _emailCtrl = TextEditingController();
|
|
final _passCtrl = TextEditingController();
|
|
bool _isLogin = true;
|
|
bool _busy = false;
|
|
String? _error;
|
|
String? _bioMsg;
|
|
bool _biometricAvailable = false;
|
|
bool _biometricEnabled = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initMode();
|
|
}
|
|
|
|
/// Decide login vs register based on whether an account exists.
|
|
Future<void> _initMode() async {
|
|
final registered = await widget.auth.isRegistered();
|
|
if (!mounted) return;
|
|
final avail = await widget.auth.canUseBiometrics();
|
|
final enabled = await widget.auth.isBiometricEnabled();
|
|
setState(() {
|
|
_isLogin = registered;
|
|
_biometricAvailable = avail;
|
|
_biometricEnabled = enabled;
|
|
});
|
|
// If already registered and biometric is enabled, prompt fingerprint
|
|
// automatically (unless a valid session already skips to dashboard).
|
|
if (registered && enabled) {
|
|
final timeout = await widget.auth.getTimeoutMinutes();
|
|
final valid = await widget.auth.hasValidSession(timeout);
|
|
if (!valid) {
|
|
_promptBiometricOnOpen();
|
|
return; // skip silent-auth path; biometric handles entry
|
|
}
|
|
}
|
|
_trySilentAuth();
|
|
}
|
|
|
|
/// Auto-prompt fingerprint on open when enabled; on success go to dashboard
|
|
/// (email may be empty for google-less accounts, fallback to stored email).
|
|
Future<void> _promptBiometricOnOpen() async {
|
|
final (ok, err) = await widget.auth.authenticateWithBiometrics();
|
|
if (!mounted) return;
|
|
if (ok) {
|
|
final email = await widget.auth.getEmail() ?? '';
|
|
_goToDashboard(email);
|
|
} else {
|
|
// fingerprint failed/cancelled -> let user login manually
|
|
setState(() {
|
|
_bioMsg = 'Gagal verifikasi sidik jari. Silakan masuk manual.';
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Manual retry via button: show the REAL error so we can diagnose
|
|
/// why the OS dialog didn't appear.
|
|
Future<void> _retryBiometric() async {
|
|
setState(() => _bioMsg = null);
|
|
final (ok, err) = await widget.auth.authenticateWithBiometrics();
|
|
if (!mounted) return;
|
|
if (ok) {
|
|
final email = await widget.auth.getEmail() ?? '';
|
|
_goToDashboard(email);
|
|
} else {
|
|
setState(() {
|
|
_bioMsg = 'Sidik jari gagal: ${err ?? "dibatalkan"}';
|
|
});
|
|
}
|
|
}
|
|
|
|
/// On open: if a valid session is present, go straight to Dashboard.
|
|
Future<void> _trySilentAuth() async {
|
|
final timeout = await widget.auth.getTimeoutMinutes();
|
|
final valid = await widget.auth.hasValidSession(timeout);
|
|
if (valid && mounted) {
|
|
final email = await widget.auth.getEmail() ?? '';
|
|
_goToDashboard(email);
|
|
}
|
|
}
|
|
|
|
void _goToDashboard(String email) {
|
|
if (!mounted) return;
|
|
widget.auth.openSession(); // refresh session timestamp
|
|
Navigator.of(context).pushReplacement(
|
|
MaterialPageRoute(
|
|
builder: (_) => DashboardScreen(
|
|
auth: widget.auth,
|
|
theme: Provider.of<ThemeProvider>(context, listen: false),
|
|
email: email,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
setState(() => _busy = true);
|
|
final email = _emailCtrl.text.trim();
|
|
final pass = _passCtrl.text;
|
|
if (email.isEmpty || pass.isEmpty) {
|
|
setState(() => _error = 'Email & password wajib diisi');
|
|
_busy = false;
|
|
return;
|
|
}
|
|
try {
|
|
if (_isLogin) {
|
|
final stored = await widget.auth.getAppPassword();
|
|
final ok = await widget.auth.verifyPassword(pass);
|
|
if (stored == null) {
|
|
setState(() => _error = 'Belum terdaftar, silakan registrasi');
|
|
} else if (!ok) {
|
|
setState(() => _error = 'Password aplikasi salah');
|
|
} else {
|
|
_goToDashboard(email);
|
|
}
|
|
} else {
|
|
await widget.auth.registerWithEmail(email, pass);
|
|
_goToDashboard(email);
|
|
}
|
|
} catch (e) {
|
|
setState(() => _error = e.toString());
|
|
} finally {
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Login Vault')),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(_isLogin ? 'Masuk' : 'Daftar',
|
|
style: Theme.of(context).textTheme.headlineSmall),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: _emailCtrl,
|
|
keyboardType: TextInputType.emailAddress,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Email', border: OutlineInputBorder()),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _passCtrl,
|
|
obscureText: true,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Password aplikasi', border: OutlineInputBorder()),
|
|
),
|
|
const SizedBox(height: 8),
|
|
if (_error != null)
|
|
Text(_error!, style: const TextStyle(color: Colors.red)),
|
|
if (_bioMsg != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Text(_bioMsg!,
|
|
style: const TextStyle(color: Colors.grey)),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_busy
|
|
? const CircularProgressIndicator()
|
|
: Column(
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: _submit,
|
|
child: Text(_isLogin ? 'Masuk' : 'Daftar'),
|
|
),
|
|
// Biometric retry button (only when enabled + registered)
|
|
if (_isLogin &&
|
|
_biometricAvailable &&
|
|
_biometricEnabled)
|
|
TextButton.icon(
|
|
onPressed: _retryBiometric,
|
|
icon: const Icon(Icons.fingerprint),
|
|
label: const Text('Gunakan Sidik Jari'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|