mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 09:08:35 +02:00
fix(config): refresh announcements and remote content
This commit is contained in:
@@ -9,7 +9,9 @@ import 'package:spotiflac_android/widgets/donate_icons.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
|
||||
|
||||
class DonatePage extends StatefulWidget {
|
||||
const DonatePage({super.key});
|
||||
final AppRemoteConfigService? remoteConfigService;
|
||||
|
||||
const DonatePage({super.key, this.remoteConfigService});
|
||||
|
||||
@override
|
||||
State<DonatePage> createState() => _DonatePageState();
|
||||
@@ -26,11 +28,11 @@ class _DonatePageState extends State<DonatePage> {
|
||||
if (_hasRequestedConfig) return;
|
||||
|
||||
_hasRequestedConfig = true;
|
||||
_loadConfig(Localizations.localeOf(context).toLanguageTag());
|
||||
unawaited(_loadConfig(Localizations.localeOf(context).toLanguageTag()));
|
||||
}
|
||||
|
||||
Future<void> _loadConfig(String locale) async {
|
||||
final service = AppRemoteConfigService();
|
||||
final service = widget.remoteConfigService ?? AppRemoteConfigService();
|
||||
final cached = await service.readCachedConfig();
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -38,11 +40,9 @@ class _DonatePageState extends State<DonatePage> {
|
||||
_applyRemoteConfig(cached);
|
||||
}
|
||||
|
||||
unawaited(_refreshConfigCache(locale));
|
||||
}
|
||||
|
||||
Future<void> _refreshConfigCache(String locale) async {
|
||||
await AppRemoteConfigService().fetchConfigSnapshot(locale: locale);
|
||||
final refreshed = await service.fetchConfigSnapshot(locale: locale);
|
||||
if (!mounted || refreshed == null) return;
|
||||
_applyRemoteConfig(refreshed);
|
||||
}
|
||||
|
||||
void _applyRemoteConfig(RemoteConfigSnapshot snapshot) {
|
||||
|
||||
@@ -262,11 +262,8 @@ class AppRemoteConfigService {
|
||||
static const _cachedConfigJsonKey = 'app_remote_config_cached_json';
|
||||
static const _cachedConfigFetchedAtKey =
|
||||
'app_remote_config_cached_fetched_at';
|
||||
static const _lastFetchAttemptAtKey = 'app_remote_config_last_fetch_at';
|
||||
static const _dismissedAnnouncementIdsKey =
|
||||
'app_remote_config_dismissed_announcement_ids';
|
||||
// Config changes are rare; don't re-hit the endpoint on every app open.
|
||||
static const _fetchTtl = Duration(hours: 6);
|
||||
|
||||
final http.Client _client;
|
||||
final String endpoint;
|
||||
@@ -293,15 +290,6 @@ class AppRemoteConfigService {
|
||||
|
||||
Future<RemoteConfigSnapshot?> fetchConfigSnapshot({String? locale}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final lastFetch = DateTime.tryParse(
|
||||
prefs.getString(_lastFetchAttemptAtKey) ?? '',
|
||||
);
|
||||
if (lastFetch != null &&
|
||||
DateTime.now().difference(lastFetch) < _fetchTtl) {
|
||||
return readCachedConfig();
|
||||
}
|
||||
|
||||
final uri = Uri.parse(endpoint).replace(
|
||||
queryParameters: {
|
||||
'platform': Platform.isAndroid ? 'android' : Platform.operatingSystem,
|
||||
@@ -323,10 +311,7 @@ class AppRemoteConfigService {
|
||||
final snapshot = _parseSnapshot(response.body);
|
||||
if (snapshot == null) return null;
|
||||
|
||||
await prefs.setString(
|
||||
_lastFetchAttemptAtKey,
|
||||
DateTime.now().toIso8601String(),
|
||||
);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cachedJson = prefs.getString(_cachedConfigJsonKey);
|
||||
if (cachedJson != snapshot.rawJson) {
|
||||
await prefs.setString(_cachedConfigJsonKey, snapshot.rawJson);
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotiflac_android/screens/settings/donate_page.dart';
|
||||
import 'package:spotiflac_android/services/app_remote_config_service.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test(
|
||||
'refreshes remote config despite a legacy recent-fetch timestamp',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'app_remote_config_cached_json': jsonEncode(
|
||||
_configPayload(
|
||||
announcementId: 'cached-announcement',
|
||||
donateTitle: 'Cached donation',
|
||||
),
|
||||
),
|
||||
'app_remote_config_last_fetch_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
|
||||
var requestCount = 0;
|
||||
final service = AppRemoteConfigService(
|
||||
endpoint: 'https://example.test/config',
|
||||
client: MockClient((request) async {
|
||||
requestCount++;
|
||||
return http.Response(
|
||||
jsonEncode(
|
||||
_configPayload(
|
||||
announcementId: 'fresh-announcement',
|
||||
donateTitle: 'Fresh donation',
|
||||
),
|
||||
),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
final snapshot = await service.fetchConfigSnapshot(locale: 'en-US');
|
||||
|
||||
expect(requestCount, 1);
|
||||
expect(snapshot?.changed, isTrue);
|
||||
expect(snapshot?.config.announcement?.id, 'fresh-announcement');
|
||||
expect(snapshot?.config.donate.title, 'Fresh donation');
|
||||
},
|
||||
);
|
||||
|
||||
test('checks the endpoint again for each remote config request', () async {
|
||||
var requestCount = 0;
|
||||
final service = AppRemoteConfigService(
|
||||
endpoint: 'https://example.test/config',
|
||||
client: MockClient((request) async {
|
||||
requestCount++;
|
||||
return http.Response(
|
||||
jsonEncode(
|
||||
_configPayload(
|
||||
announcementId: 'announcement-$requestCount',
|
||||
donateTitle: 'Donation $requestCount',
|
||||
),
|
||||
),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
final first = await service.fetchConfigSnapshot(locale: 'en-US');
|
||||
final second = await service.fetchConfigSnapshot(locale: 'en-US');
|
||||
|
||||
expect(requestCount, 2);
|
||||
expect(first?.config.announcement?.id, 'announcement-1');
|
||||
expect(second?.config.announcement?.id, 'announcement-2');
|
||||
});
|
||||
|
||||
testWidgets('donate page applies a refreshed config without reopening', (
|
||||
tester,
|
||||
) async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'app_remote_config_cached_json': jsonEncode(
|
||||
_configPayload(
|
||||
announcementId: 'cached-announcement',
|
||||
donateTitle: 'Cached donation',
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
final response = Completer<http.Response>();
|
||||
final service = AppRemoteConfigService(
|
||||
endpoint: 'https://example.test/config',
|
||||
client: MockClient((request) => response.future),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: DonatePage(remoteConfigService: service)),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Cached donation'), findsOneWidget);
|
||||
|
||||
response.complete(
|
||||
http.Response(
|
||||
jsonEncode(
|
||||
_configPayload(
|
||||
announcementId: 'fresh-announcement',
|
||||
donateTitle: 'Fresh donation',
|
||||
),
|
||||
),
|
||||
200,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Cached donation'), findsNothing);
|
||||
expect(find.text('Fresh donation'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, Object?> _configPayload({
|
||||
required String announcementId,
|
||||
required String donateTitle,
|
||||
}) {
|
||||
return {
|
||||
'announcement': {
|
||||
'id': announcementId,
|
||||
'enabled': true,
|
||||
'title': 'Announcement',
|
||||
'message': 'Announcement message',
|
||||
},
|
||||
'donate': {
|
||||
'enabled': true,
|
||||
'title': donateTitle,
|
||||
'message': 'Donation message',
|
||||
'methods': [
|
||||
{
|
||||
'id': 'support',
|
||||
'title': 'Support',
|
||||
'subtitle': 'Support this project',
|
||||
'url': 'https://example.test/support',
|
||||
},
|
||||
],
|
||||
'supporters': <String>[],
|
||||
'notices': ['Donation notice'],
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user