fix: alot
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user