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 createState() => _LockScreenState(); } class _LockScreenState extends State 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(); if (lock.isCoolingDown) return; setState(() => _pin += digit); if (_pin.length == 4) { setState(() => _busy = true); Future.delayed(const Duration(milliseconds: 110), _submit); } } void _onBackspace() { if (_busy || _pin.isEmpty) return; setState(() => _pin = _pin.substring(0, _pin.length - 1)); } Future _submit() async { if (!mounted) return; final lock = context.read(); 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(); 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: _busy ? Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: colors.accent, ), ), const SizedBox(width: 9), Text( 'Checking…', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ], ) : 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, ), ], ), ), ), ), ), ), ), ); } }