diff --git a/lib/app/router.dart b/lib/app/router.dart index b7f2c7e..74146f2 100644 --- a/lib/app/router.dart +++ b/lib/app/router.dart @@ -21,10 +21,12 @@ GoRouter createAppRouter(PinLockViewModel pinLockViewModel) { redirect: (context, state) { if (!pinLockViewModel.hasLoaded) return null; - final needsSetup = !pinLockViewModel.isPinSet; - final isLocked = - pinLockViewModel.isPinSet && !pinLockViewModel.isUnlocked; - final mustBeOnLock = needsSetup || isLocked; + final needsPinSetup = + !pinLockViewModel.isPinSet && pinLockViewModel.pinRequired; + final isLocked = pinLockViewModel.isPinSet && + !pinLockViewModel.isUnlocked && + pinLockViewModel.pinRequired; + final mustBeOnLock = needsPinSetup || isLocked; final goingToLock = state.matchedLocation == '/lock'; diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index a9cfcba..f6b0666 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:path_provider/path_provider.dart'; + part 'app_database.g.dart'; @DataClassName('ProductRow') diff --git a/lib/main.dart b/lib/main.dart index f8184a5..e5c766b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,7 +31,12 @@ Future main() async { DeviceOrientation.landscapeRight, ]); - final pinLockViewModel = PinLockViewModel(pinLockService: PinLockService()); + final database = AppDatabase(); + + final pinLockViewModel = PinLockViewModel( + pinLockService: PinLockService(), + settingsService: DriftSettingsService(database: database), + ); await pinLockViewModel.ensureLoaded(); final appRouter = createAppRouter(pinLockViewModel); @@ -39,10 +44,7 @@ Future main() async { runApp( MultiProvider( providers: [ - Provider( - create: (_) => AppDatabase(), - dispose: (_, database) => database.close(), - ), + Provider.value(value: database), Provider( create: (context) => DriftProductService(database: context.read()), @@ -95,4 +97,4 @@ Future main() async { child: AppBootstrap(child: KoolTabApp(router: appRouter)), ), ); -} +} \ No newline at end of file diff --git a/lib/utils/app_updater.dart b/lib/utils/app_updater.dart index 49545e7..d4c197a 100644 --- a/lib/utils/app_updater.dart +++ b/lib/utils/app_updater.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:kooltab2/utils/app_update_util.dart'; import 'package:ota_update/ota_update.dart'; @@ -8,6 +10,7 @@ class UpdateChecker { static bool _hasChecked = false; static Future check(BuildContext context) async { + if (!Platform.isAndroid) return; if (_hasChecked) { debugPrint("Update already checked this boot"); return; diff --git a/lib/viewmodels/pin_lock_view_model.dart b/lib/viewmodels/pin_lock_view_model.dart index 2a7163e..992f9d2 100644 --- a/lib/viewmodels/pin_lock_view_model.dart +++ b/lib/viewmodels/pin_lock_view_model.dart @@ -1,16 +1,19 @@ import 'package:flutter/foundation.dart'; import '../services/pin_lock_service.dart'; +import '../services/settings_service.dart'; class PinLockViewModel extends ChangeNotifier { final PinLockService pinLockService; + final SettingsService? settingsService; - PinLockViewModel({required this.pinLockService}); + PinLockViewModel({required this.pinLockService, this.settingsService}); bool _isPinSet = false; bool _isUnlocked = false; bool _isLoading = false; bool _hasLoaded = false; + bool _pinRequired = true; String? _errorMessage; bool get isPinSet => _isPinSet; @@ -21,8 +24,16 @@ class PinLockViewModel extends ChangeNotifier { bool get hasLoaded => _hasLoaded; + bool get pinRequired => _pinRequired; + String? get errorMessage => _errorMessage; + void setPinRequired(bool value) { + if (_pinRequired == value) return; + _pinRequired = value; + notifyListeners(); + } + Future ensureLoaded() async { if (_hasLoaded || _isLoading) return; @@ -39,6 +50,12 @@ class PinLockViewModel extends ChangeNotifier { try { _isPinSet = await pinLockService.hasPin(); + + final svc = settingsService; + if (svc != null) { + final settings = await svc.getSettings(); + _pinRequired = settings.pinRequired; + } } catch (_) { _errorMessage = 'Could not check PIN status.'; } finally { @@ -58,6 +75,14 @@ class PinLockViewModel extends ChangeNotifier { await pinLockService.setPin(pin); _isPinSet = true; _isUnlocked = true; + _pinRequired = true; + + final svc = settingsService; + if (svc != null) { + final current = await svc.getSettings(); + await svc.saveSettings(current.copyWith(pinRequired: true)); + } + notifyListeners(); return true; } catch (_) { @@ -118,6 +143,14 @@ class PinLockViewModel extends ChangeNotifier { await pinLockService.clearPin(); _isPinSet = false; + _pinRequired = false; + + final svc = settingsService; + if (svc != null) { + final current = await svc.getSettings(); + await svc.saveSettings(current.copyWith(pinRequired: false)); + } + notifyListeners(); return true; } diff --git a/lib/views/bar_screen_view.dart b/lib/views/bar_screen_view.dart index e0d001a..aa248ff 100644 --- a/lib/views/bar_screen_view.dart +++ b/lib/views/bar_screen_view.dart @@ -62,14 +62,16 @@ class _BarScreenViewState extends State { onPressed: () => context.go('/settings'), icon: const Icon(Icons.settings), ), - const SizedBox(width: 6), - IconButton( - tooltip: 'logout', - onPressed: () { - Provider.of(context, listen: false).lock(); - }, - icon: const Icon(Icons.logout), - ), + if (context.watch().isPinSet) ...[ + const SizedBox(width: 6), + IconButton( + tooltip: 'Logout', + onPressed: () { + Provider.of(context, listen: false).lock(); + }, + icon: const Icon(Icons.logout), + ), + ], ], actionsPadding: const EdgeInsets.symmetric(horizontal: 8), ), diff --git a/lib/views/pin_lock_view.dart b/lib/views/pin_lock_view.dart index 17b5e9a..f14b956 100644 --- a/lib/views/pin_lock_view.dart +++ b/lib/views/pin_lock_view.dart @@ -242,30 +242,32 @@ class _PinKeypad extends StatelessWidget { children: [ for (final row in _rows) Padding( - padding: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric(vertical: 8), child: Row( - spacing: 16, mainAxisAlignment: MainAxisAlignment.center, children: [ - for (final digit in row) + for (final digit in row) ...[ _KeypadButton( label: digit, onTap: enabled ? () => onDigit(digit) : null, ), + const SizedBox(width: 20), + ], ], ), ), Padding( - padding: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric(vertical: 8), child: Row( - spacing: 16, mainAxisAlignment: MainAxisAlignment.center, children: [ const _KeypadButton(isSpacer: true, onTap: null), + const SizedBox(width: 20), _KeypadButton( label: '0', onTap: enabled ? () => onDigit('0') : null, ), + const SizedBox(width: 20), _KeypadButton( icon: Icons.backspace_outlined, onTap: enabled ? onBackspace : null, @@ -294,14 +296,14 @@ class _KeypadButton extends StatelessWidget { @override Widget build(BuildContext context) { if (isSpacer) { - return const SizedBox(width: 72, height: 72); + return const SizedBox(width: 80, height: 80); } final scheme = Theme.of(context).colorScheme; return SizedBox( - width: 72, - height: 72, + width: 80, + height: 80, child: Material( color: scheme.onSurface.withValues(alpha: 0.04), shape: const CircleBorder(), diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index e7288ac..59d27bd 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -55,32 +57,37 @@ class _SettingsScreenViewState extends State { }); } - Future _promptPin(String title, {String hint = 'Enter PIN'}) { + Future _promptPin(String title, {String hint = 'Enter PIN (4 digits)'}) { final controller = TextEditingController(); return showDialog( context: context, builder: (context) => AlertDialog( title: Text(title), - content: TextField( - controller: controller, - autofocus: true, - obscureText: true, - keyboardType: TextInputType.number, - maxLength: 6, - decoration: InputDecoration(hintText: hint), + contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), + content: SizedBox( + width: 320, + child: TextField( + controller: controller, + autofocus: true, + obscureText: true, + keyboardType: TextInputType.number, + maxLength: 4, + decoration: InputDecoration(hintText: hint), + ), ), + actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 12), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), + const SizedBox(width: 12), FilledButton( onPressed: () => Navigator.pop(context, controller.text), child: const Text('Confirm'), ), ], - actionsPadding: const EdgeInsets.symmetric(horizontal: 8), ), ); } @@ -93,11 +100,11 @@ class _SettingsScreenViewState extends State { } Future _enablePin() async { - final pin = await _promptPin('Set PIN Code', hint: '4–6 digits'); + final pin = await _promptPin('Set PIN Code', hint: '4 digits'); if (pin == null) return; - if (pin.length < 4) { - _showError('PIN must be at least 4 digits'); + if (pin.length != 4) { + _showError('PIN must be exactly 4 digits'); return; } @@ -109,6 +116,7 @@ class _SettingsScreenViewState extends State { return; } + pinLockViewModel.setPinRequired(true); await context.read().updatePinRequired(true); } @@ -116,11 +124,11 @@ class _SettingsScreenViewState extends State { final current = await _promptPin('Enter Current PIN'); if (current == null) return; - final newPin = await _promptPin('Enter New PIN', hint: '4–6 digits'); + final newPin = await _promptPin('Enter New PIN', hint: '4 digits'); if (newPin == null) return; - if (newPin.length < 4) { - _showError('PIN must be at least 4 digits'); + if (newPin.length != 4) { + _showError('PIN must be exactly 4 digits'); return; } @@ -147,6 +155,7 @@ class _SettingsScreenViewState extends State { return; } + pinLockViewModel.setPinRequired(false); await context.read().updatePinRequired(false); } @@ -252,20 +261,21 @@ class _SettingsScreenViewState extends State { ), ], ), - _SettingsSection( - title: 'Updates', - children: [ - _SettingsTile( - icon: Icons.system_update_alt_rounded, - title: settingsViewModel.checkingForUpdates - ? 'Checking for updates...' - : 'Check for updates', - onTap: settingsViewModel.checkingForUpdates - ? null - : _checkForUpdates, - ), - ], - ), + if (Platform.isAndroid) + _SettingsSection( + title: 'Updates', + children: [ + _SettingsTile( + icon: Icons.system_update_alt_rounded, + title: settingsViewModel.checkingForUpdates + ? 'Checking for updates...' + : 'Check for updates', + onTap: settingsViewModel.checkingForUpdates + ? null + : _checkForUpdates, + ), + ], + ), const SizedBox(height: 40), GestureDetector( onTap: _onVersionTap, @@ -289,6 +299,7 @@ class _SettingsScreenViewState extends State { } Future _checkForUpdates() async { + if (!Platform.isAndroid) return; final vm = context.read(); try {