Files
DriverVault/Phone App/lib/screens/lock_screen.dart
T
tajniak81andClaude Opus 4.8 b6bb6b1df0 Add a language-switch system with per-language files
Introduce a hand-rolled i18n layer across all three UIs, each reading its
text from per-language JSON files (English base + Polish + Danish). Nothing
in the converted screens hardcodes English any more.

- Web App (Vue): src/i18n/{en,pl,da}.json + index.js exposing t()/tSplit(),
  reactive to the signed-in profile locale. Every view, component, form and
  the status labels in lib/format.js go through t().
- API Server panel (Vue): src/i18n/ with its own localStorage-persisted
  language (the panel has no user profile) and a header language picker.
  Chrome, cards, login and API section titles translated; endpoint reference
  descriptions intentionally kept in English. Rebuilt embedded dist.
- Phone App (Flutter): assets/i18n/ + lib/i18n.dart loaded at startup,
  driven by AppSettings.locale. Nav, login, lock, dashboard, the full
  Settings panel (incl. language picker) and format.dart status labels
  translated; remaining detail screens fall back to English.

Language = the language half of the existing BCP-47 locale; the region half
still drives date/number/currency formatting. Missing keys fall back to
English, and plurals use Intl.PluralRules / Intl.plural so Polish gets the
correct one/few/many forms. Settings flags languages without a translation.

Tests updated to assert the localized (Polish) status wording; all pass.
See TRANSLATIONS.md for the format and how to add a language.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 20:07:48 +02:00

112 lines
3.7 KiB
Dart

import "package:flutter/material.dart";
import "../biometric.dart";
import "../i18n.dart";
import "../main.dart";
import "../theme.dart";
/// Shown when the app is authenticated but locked (biometric login is enabled
/// and the app was just launched or resumed). The session token is still valid;
/// a successful biometric check simply reveals the app. "Use password" drops the
/// session and returns to the login screen.
class LockScreen extends StatefulWidget {
const LockScreen({super.key});
@override
State<LockScreen> createState() => _LockScreenState();
}
class _LockScreenState extends State<LockScreen> {
bool _busy = false;
String? _error;
@override
void initState() {
super.initState();
// Prompt once as soon as the lock screen appears.
WidgetsBinding.instance.addPostFrameCallback((_) => _unlock());
}
Future<void> _unlock() async {
if (_busy) return;
setState(() {
_busy = true;
_error = null;
});
try {
final creds = await biometricAuth.unlock(reason: t("lock.reason"));
if (creds == null) {
// Cancelled — leave locked; the buttons let them retry or use password.
if (mounted) setState(() => _busy = false);
return;
}
// Token from a previous session is still held by ApiClient; just reveal.
authService.unlock();
} on BiometricException catch (e) {
if (mounted) setState(() => _error = e.message);
} catch (e) {
if (mounted) setState(() => _error = e.toString());
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) {
final u = authService.user;
final name = u == null ? null : (u.name.isNotEmpty ? u.name : u.email);
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: DriverVault.brand700,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(Icons.lock_outline, color: Colors.white, size: 32),
),
const SizedBox(height: 16),
Text(t("lock.title"),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, letterSpacing: -0.4)),
if (name != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(name, style: TextStyle(color: DriverVault.muted(context))),
),
const SizedBox(height: 24),
if (_error != null)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(_error!,
textAlign: TextAlign.center,
style: const TextStyle(color: DriverVault.danger)),
),
SizedBox(
width: 260,
child: FilledButton.icon(
onPressed: _busy ? null : _unlock,
icon: const Icon(Icons.fingerprint),
label: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Text(_busy ? t("lock.unlocking") : t("lock.unlock")),
),
),
),
const SizedBox(height: 8),
TextButton(
onPressed: _busy ? null : () => authService.logout(),
child: Text(t("lock.usePassword")),
),
],
),
),
),
);
}
}