| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- import 'dart:async';
- import 'dart:math' as math;
- import 'package:flutter/material.dart';
- import 'package:provider/provider.dart';
- import '../../core/theme/app_colors.dart';
- import 'lock_controller.dart';
- import 'pin_pad.dart';
- /// Full-screen gate shown only when the launcher is cold-started with a PIN
- /// configured. It never re-locks a running session.
- class LockScreen extends StatefulWidget {
- const LockScreen({super.key});
- @override
- State<LockScreen> createState() => _LockScreenState();
- }
- class _LockScreenState extends State<LockScreen>
- with SingleTickerProviderStateMixin {
- String _pin = '';
- bool _busy = false;
- Timer? _tick;
- late final AnimationController _shake = AnimationController(
- vsync: this,
- duration: const Duration(milliseconds: 380),
- );
- @override
- void initState() {
- super.initState();
- _tick = Timer.periodic(const Duration(seconds: 1), (_) {
- if (mounted) setState(() {});
- });
- }
- @override
- void dispose() {
- _tick?.cancel();
- _shake.dispose();
- super.dispose();
- }
- void _onDigit(String digit) {
- if (_busy || _pin.length >= 4) return;
- final lock = context.read<LockController>();
- if (lock.isCoolingDown) return;
- setState(() => _pin += digit);
- if (_pin.length == 4) {
- setState(() => _busy = true);
- Future<void>.delayed(const Duration(milliseconds: 110), _submit);
- }
- }
- void _onBackspace() {
- if (_busy || _pin.isEmpty) return;
- setState(() => _pin = _pin.substring(0, _pin.length - 1));
- }
- Future<void> _submit() async {
- if (!mounted) return;
- final lock = context.read<LockController>();
- final success = await lock.unlock(_pin);
- if (!mounted) return;
- if (success) {
- return; // The root widget removes this screen.
- }
- setState(() {
- _pin = '';
- _busy = false;
- });
- _shake.forward(from: 0);
- }
- @override
- Widget build(BuildContext context) {
- final colors = context.colors;
- final theme = Theme.of(context);
- final lock = context.watch<LockController>();
- final error = lock.isCoolingDown
- ? 'Too many attempts. Try again in ${lock.cooldownSecondsRemaining}s.'
- : lock.errorMessage;
- return PopScope(
- canPop: false,
- child: Scaffold(
- backgroundColor: colors.canvas,
- body: Container(
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.topLeft,
- end: Alignment.bottomRight,
- colors: Theme.of(context).brightness == Brightness.dark
- ? [colors.canvas, colors.canvasDeep]
- : [colors.canvas, const Color(0xFFF7F5EF)],
- ),
- ),
- child: SafeArea(
- child: Center(
- child: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(
- horizontal: 32,
- vertical: 28,
- ),
- child: ConstrainedBox(
- constraints: const BoxConstraints(maxWidth: 420),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- const BrandMark(size: 7, gap: 6),
- const SizedBox(height: 16),
- Text(
- 'CarDash',
- style: theme.textTheme.titleLarge?.copyWith(
- letterSpacing: 0.2,
- color: colors.inkMuted,
- ),
- ),
- const SizedBox(height: 34),
- Text('Enter PIN', style: theme.textTheme.headlineLarge),
- const SizedBox(height: 8),
- Text(
- 'CarDash is locked.',
- style: theme.textTheme.bodyMedium?.copyWith(
- color: colors.inkMuted,
- ),
- ),
- const SizedBox(height: 30),
- AnimatedBuilder(
- animation: _shake,
- builder: (context, child) {
- final progress = _shake.value;
- final offset =
- math.sin(progress * math.pi * 6) *
- 9 *
- (1 - progress);
- return Transform.translate(
- offset: Offset(offset, 0),
- child: child,
- );
- },
- child: PinDots(
- length: 4,
- filled: _pin.length,
- error: error != null,
- ),
- ),
- const SizedBox(height: 14),
- SizedBox(
- height: 42,
- child: AnimatedSwitcher(
- duration: const Duration(milliseconds: 160),
- child: Text(
- error ?? 'Enter your four-digit PIN',
- key: ValueKey(error ?? 'hint'),
- textAlign: TextAlign.center,
- style: theme.textTheme.bodySmall?.copyWith(
- color: error == null
- ? colors.inkFaint
- : colors.danger,
- ),
- ),
- ),
- ),
- const SizedBox(height: 12),
- PinKeypad(
- enabled: !_busy && !lock.isCoolingDown,
- onDigit: _onDigit,
- onBackspace: _onBackspace,
- ),
- ],
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- );
- }
- }
|