| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- 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<PinSetupScreen> createState() => _PinSetupScreenState();
- }
- class _PinSetupScreenState extends State<PinSetupScreen>
- 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<LockController>();
- if (lock.isCoolingDown) return;
- setState(() {
- _error = null;
- _pin += digit;
- });
- if (_pin.length == 4) {
- setState(() => _busy = true);
- Future<void>.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<void> _advance() async {
- if (!mounted) return;
- final lock = context.read<LockController>();
- 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<LockController>();
- 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: 83,
- height: 83,
- 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: 36,
- 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,
- ),
- ],
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- );
- }
- }
|