feat: more easter eggs + version checking

This commit is contained in:
2026-10-27 00:17:18 +01:00
parent 4d9ba67eb1
commit 1aee0541c4
14 changed files with 178 additions and 6 deletions
+29 -4
View File
@@ -26,6 +26,13 @@ class UnauthorizedException extends ApiException {
: super(401, message ?? 'Unauthorized');
}
/// This app is older than the backoffice accepts; it has to be updated
/// before it can sync again. The token stays valid.
class UpdateRequiredException extends ApiException {
const UpdateRequiredException({super.code, String? message})
: super(426, message ?? 'App update required');
}
class RateLimitedException extends ApiException {
final Duration retryAfter;
@@ -43,15 +50,26 @@ class KoolTapApiClient {
final http.Client _client;
final String baseUrl;
KoolTapApiClient({http.Client? client, this.baseUrl = kRemoteServerUrl})
: _client = client ?? http.Client();
/// Sent with every request so the backoffice can refuse an app that is too
/// old (`426 app_update_required`).
final String? appVersion;
KoolTapApiClient({
http.Client? client,
this.baseUrl = kRemoteServerUrl,
this.appVersion,
}) : _client = client ?? http.Client();
Map<String, String> get _versionHeader => {
'X-App-Version': ?appVersion,
};
static const _timeout = Duration(seconds: 15);
static const _defaultRetryAfter = Duration(seconds: 60);
Future<List<Venue>> getVenues() async {
final response = await _client
.get(Uri.parse('$baseUrl/venues/'))
.get(Uri.parse('$baseUrl/venues/'), headers: _versionHeader)
.timeout(_timeout);
final data = _decode(response) as List<dynamic>;
@@ -117,6 +135,7 @@ class KoolTapApiClient {
'POST',
Uri.parse('$baseUrl/devices/$deviceId/images'),
)
..headers.addAll(_versionHeader)
..headers['Authorization'] = 'Bearer $token'
..files.add(
http.MultipartFile.fromBytes('file', bytes, filename: filename),
@@ -133,7 +152,7 @@ class KoolTapApiClient {
/// A product picture (public: pictures never change at their address).
Future<List<int>> downloadImage(String imageId) async {
final response = await _client
.get(Uri.parse('$baseUrl/images/$imageId'))
.get(Uri.parse('$baseUrl/images/$imageId'), headers: _versionHeader)
.timeout(_timeout * 2);
if (response.statusCode != 200) {
throw ApiException(response.statusCode, 'Image $imageId not available');
@@ -151,6 +170,7 @@ class KoolTapApiClient {
Uri.parse('$baseUrl$path'),
headers: {
'Content-Type': 'application/json',
..._versionHeader,
if (token != null) 'Authorization': 'Bearer $token',
},
body: jsonEncode(body),
@@ -170,6 +190,11 @@ class KoolTapApiClient {
throw UnauthorizedException(code: error.code, message: error.message);
}
if (status == 426) {
final error = _error(response);
throw UpdateRequiredException(code: error.code, message: error.message);
}
if (status == 429) {
final seconds = int.tryParse(response.headers['retry-after'] ?? '');
throw RateLimitedException(
+9
View File
@@ -273,6 +273,13 @@ class RemoteSyncService {
stopPeriodicSync();
_emit(RemoteSyncEventType.unauthorized);
return;
} on UpdateRequiredException catch (e) {
// Retrying can't help until the app is updated. The token and every
// outbox stay, so nothing is lost; a manual sync or restart tries again
debugPrint('RemoteSyncService: app update required');
stopPeriodicSync();
_emit(RemoteSyncEventType.updateRequired, message: e.message);
return;
} on RateLimitedException catch (e) {
nextDelay = await _recordFailure(e, retryAfter: e.retryAfter);
} catch (e, stack) {
@@ -328,6 +335,8 @@ class RemoteSyncService {
);
} on UnauthorizedException {
rethrow;
} on UpdateRequiredException {
rethrow;
} catch (e) {
debugPrint('RemoteSyncService: picture upload failed: $e');
}