import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/theme/app_colors.dart'; import '../../core/widgets/app_icon.dart'; import '../../core/widgets/pressable.dart'; import 'lock_controller.dart'; import 'pin_pad.dart'; enum PinSetupMode { set, change, remove } /// Set, change or remove the four-digit PIN. The PIN is only used when the /// launcher is cold-started; this screen never locks the current session. class PinSetupScreen extends StatefulWidget { const PinSetupScreen({super.key, required this.mode}); final PinSetupMode mode; @override State createState() => _PinSetupScreenState(); } class _PinSetupScreenState extends State with SingleTickerProviderStateMixin { String _pin = ''; int _step = 0; String? _firstPin; String? _currentPin; bool _busy = false; String? _error; late final AnimationController _shake = AnimationController( vsync: this, duration: const Duration(milliseconds: 380), ); @override void dispose() { _shake.dispose(); super.dispose(); } int get _stepCount { switch (widget.mode) { case PinSetupMode.set: return 2; case PinSetupMode.change: return 3; case PinSetupMode.remove: return 1; } } String get _title { switch (widget.mode) { case PinSetupMode.set: return _step == 0 ? 'Choose a PIN' : 'Confirm your PIN'; case PinSetupMode.change: return switch (_step) { 0 => 'Enter current PIN', 1 => 'Choose a new PIN', _ => 'Confirm your new PIN', }; case PinSetupMode.remove: return 'Enter current PIN'; } } String get _description { switch (widget.mode) { case PinSetupMode.set: return _step == 0 ? 'Four digits. You will be asked for this the next time CarDash starts.' : 'Enter the same four digits again.'; case PinSetupMode.change: return switch (_step) { 0 => 'Confirm your current PIN before choosing a new one.', 1 => 'Four digits. The new PIN is used the next time CarDash starts.', _ => 'Enter the new four digits again.', }; case PinSetupMode.remove: return 'Confirm your current PIN. PIN protection will be turned off.'; } } void _onDigit(String digit) { if (_busy || _pin.length >= 4) return; final lock = context.read(); if (lock.isCoolingDown) return; setState(() { _error = null; _pin += digit; }); if (_pin.length == 4) { setState(() => _busy = true); Future.delayed(const Duration(milliseconds: 110), _advance); } } void _onBackspace() { if (_busy || _pin.isEmpty) return; setState(() => _pin = _pin.substring(0, _pin.length - 1)); } void _fail(String message) { setState(() { _pin = ''; _busy = false; _error = message; }); _shake.forward(from: 0); } Future _advance() async { if (!mounted) return; final lock = context.read(); switch (widget.mode) { case PinSetupMode.set: if (_step == 0) { setState(() { _firstPin = _pin; _pin = ''; _step = 1; _busy = false; _error = null; }); return; } if (_pin != _firstPin) { _fail('PINs do not match.'); return; } final error = await lock.setPin(_firstPin!); if (!mounted) return; if (error != null) { _fail(error); return; } Navigator.of(context).pop(true); case PinSetupMode.change: if (_step == 0) { final valid = await lock.verifyPin(_pin); if (!mounted) return; if (!valid) { _fail(lock.errorMessage ?? 'Current PIN is incorrect.'); return; } setState(() { _currentPin = _pin; _pin = ''; _step = 1; _busy = false; _error = null; }); return; } if (_step == 1) { setState(() { _firstPin = _pin; _pin = ''; _step = 2; _busy = false; _error = null; }); return; } if (_pin != _firstPin) { _fail('PINs do not match.'); return; } final error = await lock.changePin( currentPin: _currentPin!, newPin: _firstPin!, ); if (!mounted) return; if (error != null) { _fail(error); return; } Navigator.of(context).pop(true); case PinSetupMode.remove: final error = await lock.removePin(_pin); if (!mounted) return; if (error != null) { _fail(error); return; } Navigator.of(context).pop(true); } } @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.' : _error; return 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: Padding( padding: const EdgeInsets.fromLTRB(32, 22, 32, 22), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Pressable( onTap: () => Navigator.of(context).pop(), semanticLabel: 'Back', pressedScale: 0.92, child: Container( width: 46, height: 46, decoration: BoxDecoration( color: colors.surface, shape: BoxShape.circle, border: Border.all(color: colors.border), ), alignment: Alignment.center, child: AppIcon( 'assets/icons/arrow-left.svg', size: 20, color: colors.ink, ), ), ), const SizedBox(width: 16), Text( 'PIN', style: theme.textTheme.titleLarge?.copyWith( color: colors.inkMuted, ), ), const Spacer(), if (_stepCount > 1) Text( '${_step + 1} of $_stepCount', style: theme.textTheme.labelMedium?.copyWith( color: colors.inkFaint, ), ), ], ), Expanded( child: Center( child: SingleChildScrollView( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: Column( mainAxisSize: MainAxisSize.min, children: [ const BrandMark(size: 7, gap: 6), const SizedBox(height: 26), Text( _title, textAlign: TextAlign.center, style: theme.textTheme.headlineLarge, ), const SizedBox(height: 8), Text( _description, textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( color: colors.inkMuted, ), ), const SizedBox(height: 28), 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, ), ), ], ) : Text( error ?? 'Four digits', textAlign: TextAlign.center, style: theme.textTheme.bodySmall ?.copyWith( color: error == null ? colors.inkFaint : colors.danger, ), ), ), const SizedBox(height: 6), PinKeypad( enabled: !_busy && !lock.isCoolingDown, onDigit: _onDigit, onBackspace: _onBackspace, ), ], ), ), ), ), ), ], ), ), ), ), ); } }