fix: pincode functionality

This commit is contained in:
2026-07-29 22:43:38 +02:00
parent 3cf4787e8c
commit a732e2d07f
8 changed files with 112 additions and 56 deletions
+6 -4
View File
@@ -21,10 +21,12 @@ GoRouter createAppRouter(PinLockViewModel pinLockViewModel) {
redirect: (context, state) { redirect: (context, state) {
if (!pinLockViewModel.hasLoaded) return null; if (!pinLockViewModel.hasLoaded) return null;
final needsSetup = !pinLockViewModel.isPinSet; final needsPinSetup =
final isLocked = !pinLockViewModel.isPinSet && pinLockViewModel.pinRequired;
pinLockViewModel.isPinSet && !pinLockViewModel.isUnlocked; final isLocked = pinLockViewModel.isPinSet &&
final mustBeOnLock = needsSetup || isLocked; !pinLockViewModel.isUnlocked &&
pinLockViewModel.pinRequired;
final mustBeOnLock = needsPinSetup || isLocked;
final goingToLock = state.matchedLocation == '/lock'; final goingToLock = state.matchedLocation == '/lock';
+1
View File
@@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'; import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
part 'app_database.g.dart'; part 'app_database.g.dart';
@DataClassName('ProductRow') @DataClassName('ProductRow')
+7 -5
View File
@@ -31,7 +31,12 @@ Future<void> main() async {
DeviceOrientation.landscapeRight, DeviceOrientation.landscapeRight,
]); ]);
final pinLockViewModel = PinLockViewModel(pinLockService: PinLockService()); final database = AppDatabase();
final pinLockViewModel = PinLockViewModel(
pinLockService: PinLockService(),
settingsService: DriftSettingsService(database: database),
);
await pinLockViewModel.ensureLoaded(); await pinLockViewModel.ensureLoaded();
final appRouter = createAppRouter(pinLockViewModel); final appRouter = createAppRouter(pinLockViewModel);
@@ -39,10 +44,7 @@ Future<void> main() async {
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
Provider<AppDatabase>( Provider<AppDatabase>.value(value: database),
create: (_) => AppDatabase(),
dispose: (_, database) => database.close(),
),
Provider<ProductService>( Provider<ProductService>(
create: (context) => create: (context) =>
DriftProductService(database: context.read<AppDatabase>()), DriftProductService(database: context.read<AppDatabase>()),
+3
View File
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kooltab2/utils/app_update_util.dart'; import 'package:kooltab2/utils/app_update_util.dart';
import 'package:ota_update/ota_update.dart'; import 'package:ota_update/ota_update.dart';
@@ -8,6 +10,7 @@ class UpdateChecker {
static bool _hasChecked = false; static bool _hasChecked = false;
static Future<void> check(BuildContext context) async { static Future<void> check(BuildContext context) async {
if (!Platform.isAndroid) return;
if (_hasChecked) { if (_hasChecked) {
debugPrint("Update already checked this boot"); debugPrint("Update already checked this boot");
return; return;
+34 -1
View File
@@ -1,16 +1,19 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../services/pin_lock_service.dart'; import '../services/pin_lock_service.dart';
import '../services/settings_service.dart';
class PinLockViewModel extends ChangeNotifier { class PinLockViewModel extends ChangeNotifier {
final PinLockService pinLockService; final PinLockService pinLockService;
final SettingsService? settingsService;
PinLockViewModel({required this.pinLockService}); PinLockViewModel({required this.pinLockService, this.settingsService});
bool _isPinSet = false; bool _isPinSet = false;
bool _isUnlocked = false; bool _isUnlocked = false;
bool _isLoading = false; bool _isLoading = false;
bool _hasLoaded = false; bool _hasLoaded = false;
bool _pinRequired = true;
String? _errorMessage; String? _errorMessage;
bool get isPinSet => _isPinSet; bool get isPinSet => _isPinSet;
@@ -21,8 +24,16 @@ class PinLockViewModel extends ChangeNotifier {
bool get hasLoaded => _hasLoaded; bool get hasLoaded => _hasLoaded;
bool get pinRequired => _pinRequired;
String? get errorMessage => _errorMessage; String? get errorMessage => _errorMessage;
void setPinRequired(bool value) {
if (_pinRequired == value) return;
_pinRequired = value;
notifyListeners();
}
Future<void> ensureLoaded() async { Future<void> ensureLoaded() async {
if (_hasLoaded || _isLoading) return; if (_hasLoaded || _isLoading) return;
@@ -39,6 +50,12 @@ class PinLockViewModel extends ChangeNotifier {
try { try {
_isPinSet = await pinLockService.hasPin(); _isPinSet = await pinLockService.hasPin();
final svc = settingsService;
if (svc != null) {
final settings = await svc.getSettings();
_pinRequired = settings.pinRequired;
}
} catch (_) { } catch (_) {
_errorMessage = 'Could not check PIN status.'; _errorMessage = 'Could not check PIN status.';
} finally { } finally {
@@ -58,6 +75,14 @@ class PinLockViewModel extends ChangeNotifier {
await pinLockService.setPin(pin); await pinLockService.setPin(pin);
_isPinSet = true; _isPinSet = true;
_isUnlocked = true; _isUnlocked = true;
_pinRequired = true;
final svc = settingsService;
if (svc != null) {
final current = await svc.getSettings();
await svc.saveSettings(current.copyWith(pinRequired: true));
}
notifyListeners(); notifyListeners();
return true; return true;
} catch (_) { } catch (_) {
@@ -118,6 +143,14 @@ class PinLockViewModel extends ChangeNotifier {
await pinLockService.clearPin(); await pinLockService.clearPin();
_isPinSet = false; _isPinSet = false;
_pinRequired = false;
final svc = settingsService;
if (svc != null) {
final current = await svc.getSettings();
await svc.saveSettings(current.copyWith(pinRequired: false));
}
notifyListeners(); notifyListeners();
return true; return true;
} }
+3 -1
View File
@@ -62,15 +62,17 @@ class _BarScreenViewState extends State<BarScreenView> {
onPressed: () => context.go('/settings'), onPressed: () => context.go('/settings'),
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
), ),
if (context.watch<PinLockViewModel>().isPinSet) ...[
const SizedBox(width: 6), const SizedBox(width: 6),
IconButton( IconButton(
tooltip: 'logout', tooltip: 'Logout',
onPressed: () { onPressed: () {
Provider.of<PinLockViewModel>(context, listen: false).lock(); Provider.of<PinLockViewModel>(context, listen: false).lock();
}, },
icon: const Icon(Icons.logout), icon: const Icon(Icons.logout),
), ),
], ],
],
actionsPadding: const EdgeInsets.symmetric(horizontal: 8), actionsPadding: const EdgeInsets.symmetric(horizontal: 8),
), ),
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
+10 -8
View File
@@ -242,30 +242,32 @@ class _PinKeypad extends StatelessWidget {
children: [ children: [
for (final row in _rows) for (final row in _rows)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.symmetric(vertical: 8),
child: Row( child: Row(
spacing: 16,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
for (final digit in row) for (final digit in row) ...[
_KeypadButton( _KeypadButton(
label: digit, label: digit,
onTap: enabled ? () => onDigit(digit) : null, onTap: enabled ? () => onDigit(digit) : null,
), ),
const SizedBox(width: 20),
],
], ],
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.symmetric(vertical: 8),
child: Row( child: Row(
spacing: 16,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const _KeypadButton(isSpacer: true, onTap: null), const _KeypadButton(isSpacer: true, onTap: null),
const SizedBox(width: 20),
_KeypadButton( _KeypadButton(
label: '0', label: '0',
onTap: enabled ? () => onDigit('0') : null, onTap: enabled ? () => onDigit('0') : null,
), ),
const SizedBox(width: 20),
_KeypadButton( _KeypadButton(
icon: Icons.backspace_outlined, icon: Icons.backspace_outlined,
onTap: enabled ? onBackspace : null, onTap: enabled ? onBackspace : null,
@@ -294,14 +296,14 @@ class _KeypadButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isSpacer) { if (isSpacer) {
return const SizedBox(width: 72, height: 72); return const SizedBox(width: 80, height: 80);
} }
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
return SizedBox( return SizedBox(
width: 72, width: 80,
height: 72, height: 80,
child: Material( child: Material(
color: scheme.onSurface.withValues(alpha: 0.04), color: scheme.onSurface.withValues(alpha: 0.04),
shape: const CircleBorder(), shape: const CircleBorder(),
+21 -10
View File
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -55,32 +57,37 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
}); });
} }
Future<String?> _promptPin(String title, {String hint = 'Enter PIN'}) { Future<String?> _promptPin(String title, {String hint = 'Enter PIN (4 digits)'}) {
final controller = TextEditingController(); final controller = TextEditingController();
return showDialog<String>( return showDialog<String>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(title), title: Text(title),
content: TextField( contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0),
content: SizedBox(
width: 320,
child: TextField(
controller: controller, controller: controller,
autofocus: true, autofocus: true,
obscureText: true, obscureText: true,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
maxLength: 6, maxLength: 4,
decoration: InputDecoration(hintText: hint), decoration: InputDecoration(hintText: hint),
), ),
),
actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Cancel'), child: const Text('Cancel'),
), ),
const SizedBox(width: 12),
FilledButton( FilledButton(
onPressed: () => Navigator.pop(context, controller.text), onPressed: () => Navigator.pop(context, controller.text),
child: const Text('Confirm'), child: const Text('Confirm'),
), ),
], ],
actionsPadding: const EdgeInsets.symmetric(horizontal: 8),
), ),
); );
} }
@@ -93,11 +100,11 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
} }
Future<void> _enablePin() async { Future<void> _enablePin() async {
final pin = await _promptPin('Set PIN Code', hint: '46 digits'); final pin = await _promptPin('Set PIN Code', hint: '4 digits');
if (pin == null) return; if (pin == null) return;
if (pin.length < 4) { if (pin.length != 4) {
_showError('PIN must be at least 4 digits'); _showError('PIN must be exactly 4 digits');
return; return;
} }
@@ -109,6 +116,7 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
return; return;
} }
pinLockViewModel.setPinRequired(true);
await context.read<SettingsViewModel>().updatePinRequired(true); await context.read<SettingsViewModel>().updatePinRequired(true);
} }
@@ -116,11 +124,11 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
final current = await _promptPin('Enter Current PIN'); final current = await _promptPin('Enter Current PIN');
if (current == null) return; if (current == null) return;
final newPin = await _promptPin('Enter New PIN', hint: '46 digits'); final newPin = await _promptPin('Enter New PIN', hint: '4 digits');
if (newPin == null) return; if (newPin == null) return;
if (newPin.length < 4) { if (newPin.length != 4) {
_showError('PIN must be at least 4 digits'); _showError('PIN must be exactly 4 digits');
return; return;
} }
@@ -147,6 +155,7 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
return; return;
} }
pinLockViewModel.setPinRequired(false);
await context.read<SettingsViewModel>().updatePinRequired(false); await context.read<SettingsViewModel>().updatePinRequired(false);
} }
@@ -252,6 +261,7 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
), ),
], ],
), ),
if (Platform.isAndroid)
_SettingsSection( _SettingsSection(
title: 'Updates', title: 'Updates',
children: [ children: [
@@ -289,6 +299,7 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
} }
Future<void> _checkForUpdates() async { Future<void> _checkForUpdates() async {
if (!Platform.isAndroid) return;
final vm = context.read<SettingsViewModel>(); final vm = context.read<SettingsViewModel>();
try { try {