fix: alot

This commit is contained in:
2026-10-19 21:41:34 +02:00
parent 30dd74af90
commit b0074aeb3f
40 changed files with 1979 additions and 405 deletions
+19 -7
View File
@@ -19,22 +19,34 @@ abstract class ExportService {
/// failed backup is logged but never interrupts the bar.
class DefaultExportService implements ExportService {
final BarTabService barTabService;
final Future<String?> Function()? authToken;
final http.Client _client;
DefaultExportService({required this.barTabService, http.Client? client})
: _client = client ?? http.Client();
DefaultExportService({
required this.barTabService,
this.authToken,
http.Client? client,
}) : _client = client ?? http.Client();
static const _timeout = Duration(seconds: 30);
@override
Future<void> uploadToServer() async {
try {
final closedTabs = await barTabService.getClosedTabs();
final appVersion = (await PackageInfo.fromPlatform()).version;
final token = await authToken?.call();
final response = await _client.post(
Uri.parse(kExportServerUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(_buildPayload(closedTabs, appVersion: appVersion)),
);
final response = await _client
.post(
Uri.parse(kExportServerUrl),
headers: {
'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
},
body: jsonEncode(_buildPayload(closedTabs, appVersion: appVersion)),
)
.timeout(_timeout);
if (response.statusCode != 200 && response.statusCode != 201) {
unawaited(
+5 -2
View File
@@ -20,6 +20,7 @@ class PinLockService {
this._saltKey = _defaultSaltKey,
this._hashKey = _defaultHashKey,
DateTime Function()? clock,
this.maxLockout = const Duration(minutes: 15),
}) : _storage = storage ?? const FlutterSecureStorage(),
_clock = clock ?? DateTime.now;
@@ -31,13 +32,15 @@ class PinLockService {
static const _freeAttempts = 5;
static const _baseLockout = Duration(seconds: 30);
static const _maxLockout = Duration(minutes: 15);
final FlutterSecureStorage _storage;
final String _saltKey;
final String _hashKey;
final DateTime Function() _clock;
/// Kept short for the bar PIN so a bartender is never locked out for long.
final Duration maxLockout;
String get _failuresKey => '${_hashKey}_failures';
String get _lockedUntilKey => '${_hashKey}_locked_until';
@@ -113,7 +116,7 @@ class PinLockService {
final exponent = min(failures - _freeAttempts, 10);
final lockout = _baseLockout * (1 << exponent);
final capped = lockout > _maxLockout ? _maxLockout : lockout;
final capped = lockout > maxLockout ? maxLockout : lockout;
await _storage.write(
key: _lockedUntilKey,
+3 -2
View File
@@ -130,12 +130,13 @@ class DriftProductService implements ProductService {
);
}
/// Only hidden, since open tabs and history still refer to it.
@override
Future<void> deleteProduct(String id) async {
final query = database.delete(database.products)
final query = database.update(database.products)
..where((product) => product.id.equals(id));
await query.go();
await query.write(const ProductsCompanion(active: Value(false)));
}
@override
+32 -11
View File
@@ -1,8 +1,10 @@
import 'dart:async';
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:sentry_flutter/sentry_flutter.dart';
import '../database/app_database.dart';
@@ -65,20 +67,29 @@ class RemoteSyncService {
DateTime? _firstPendingChange;
bool _syncQueued = false;
_Credentials? _cachedCredentials;
Future<String?> get venueId => secureStorage.read(key: _venueIdKey);
Future<bool> get isConfigured async {
final token = await secureStorage.read(key: _authTokenKey);
final venueId = await this.venueId;
return (token?.isNotEmpty ?? false) && (venueId?.isNotEmpty ?? false);
}
Future<bool> get isConfigured async => await _credentials() != null;
Future<String?> get authToken async => (await _credentials())?.token;
Future<_Credentials?> _credentials() async {
if (_cachedCredentials != null) return _cachedCredentials;
final deviceId = await secureStorage.read(key: _deviceIdKey);
final token = await secureStorage.read(key: _authTokenKey);
if (deviceId == null || token == null) return null;
final venueId = await this.venueId;
if (deviceId == null ||
token == null ||
token.isEmpty ||
venueId == null ||
venueId.isEmpty) {
return null;
}
return (deviceId: deviceId, token: token);
return _cachedCredentials = (deviceId: deviceId, token: token);
}
void startPeriodicSync() {
@@ -109,8 +120,6 @@ class RemoteSyncService {
void resume() {
_paused = false;
// Catch up right away instead of waiting for the next poll
_onTabsChanged();
}
/// Pushes tab changes to the server soon after they happen instead of
@@ -164,7 +173,7 @@ class RemoteSyncService {
}
final credentials = await _credentials();
if (credentials == null || !await isConfigured) return;
if (credentials == null) return;
_isSyncing = true;
Duration nextDelay;
@@ -176,6 +185,7 @@ class RemoteSyncService {
_emit(RemoteSyncEventType.syncCompleted);
} on UnauthorizedException {
debugPrint('RemoteSyncService: token revoked');
_cachedCredentials = null;
await secureStorage.delete(key: _authTokenKey);
stopPeriodicSync();
_emit(RemoteSyncEventType.unauthorized);
@@ -183,7 +193,9 @@ class RemoteSyncService {
} on RateLimitedException catch (e) {
nextDelay = await _recordFailure(e, retryAfter: e.retryAfter);
} catch (e, stack) {
unawaited(Sentry.captureException(e, stackTrace: stack));
if (!_isConnectivityError(e)) {
unawaited(Sentry.captureException(e, stackTrace: stack));
}
nextDelay = await _recordFailure(e);
} finally {
_isSyncing = false;
@@ -197,6 +209,13 @@ class RemoteSyncService {
}
}
/// Offline tablets are expected, not worth an error report.
bool _isConnectivityError(Object error) {
return error is IOException ||
error is TimeoutException ||
error is http.ClientException;
}
Future<Duration> _recordFailure(Object error, {Duration? retryAfter}) async {
debugPrint('RemoteSyncService: sync failed: $error');
_consecutiveFailures++;
@@ -391,9 +410,11 @@ class RemoteSyncService {
await secureStorage.write(key: _deviceIdKey, value: deviceId);
await secureStorage.write(key: _authTokenKey, value: token);
await secureStorage.write(key: _venueIdKey, value: venueId);
_cachedCredentials = (deviceId: deviceId, token: token);
}
Future<void> clearCredentials() async {
_cachedCredentials = null;
await secureStorage.delete(key: _deviceIdKey);
await secureStorage.delete(key: _authTokenKey);
await secureStorage.delete(key: _venueIdKey);