feat: show remote-config launch announcement on app start

Introduce AppRemoteConfigService which fetches a platform/version/locale-aware JSON payload from api.zarz.moe/v1/spotiflac-mobile/config and caches it in SharedPreferences. main_shell shows a one-shot announcement dialog (respecting dismissible, CTA, time window and version gates) when no update prompt is pending; dismissed IDs are persisted so each announcement surfaces only once.

Tweaks bundled in: the service health dot loses its blur halo in favour of solid Material 3 tones, and AppInfo gains the remote config endpoint constant. The share listener and SAF migration hook stay synchronous inside the post-frame callback so share-intent URLs never race the network-bound checks.

New unit tests cover the announcement CTA/active-window rules.
This commit is contained in:
zarzet
2026-05-11 01:37:10 +07:00
parent 81547013f9
commit 7845ac8be5
7 changed files with 978 additions and 266 deletions
+124
View File
@@ -4,6 +4,7 @@ import 'package:spotiflac_android/models/download_item.dart';
import 'package:spotiflac_android/models/settings.dart';
import 'package:spotiflac_android/models/theme_settings.dart';
import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/services/app_remote_config_service.dart';
import 'package:spotiflac_android/services/download_request_payload.dart';
import 'package:spotiflac_android/utils/artist_utils.dart';
import 'package:spotiflac_android/utils/mime_utils.dart';
@@ -507,4 +508,127 @@ void main() {
expect(keys, contains('C:/Music/Song'));
});
});
group('AppRemoteConfig', () {
test('parses announcement and donate payloads from API JSON', () {
final config = AppRemoteConfig.fromJson({
'announcement': {
'id': 'hello-2026',
'enabled': true,
'title': 'Server message',
'message': 'A clear message for users',
'cta_enabled': true,
'cta_label': 'Donate',
'cta_url': 'https://example.test/donate',
'starts_at': '2026-05-01T00:00:00Z',
'ends_at': '2026-06-01T00:00:00Z',
'min_version': '4.5.0',
'priority': 'high',
},
'donate': {
'enabled': true,
'title': 'Support SpotiFLAC Mobile',
'message': 'Help cover infrastructure.',
'methods': [
{
'id': 'kofi',
'title': 'Ko-fi',
'subtitle': 'ko-fi.com/example',
'url': 'https://ko-fi.com/example',
'icon': 'kofi',
'color': '#FF5E5B',
},
{
'id': 'wallet',
'title': 'USDT',
'subtitle': 'TRC20',
'wallet_address': 'T123',
'icon': 'wallet',
'color': '0xFF26A17B',
},
],
'supporters': ['Alice', 'Bob'],
'notices': ['No paywalls'],
},
});
expect(config.announcement?.id, 'hello-2026');
expect(config.announcement?.hasCta, isTrue);
expect(
config.announcement?.isActive(
now: DateTime.utc(2026, 5, 11),
currentVersion: '4.5.1',
),
isTrue,
);
expect(config.donate.title, 'Support SpotiFLAC Mobile');
expect(config.donate.methods, hasLength(2));
expect(config.donate.methods.first.color, 0xFFFF5E5B);
expect(config.donate.methods.last.isWallet, isTrue);
expect(config.donate.supporters, ['Alice', 'Bob']);
expect(config.donate.notices, ['No paywalls']);
});
test('requires enabled announcement CTA with label and url', () {
final disabledCta = RemoteAnnouncement.fromJson({
'id': 'notice',
'title': 'Notice',
'message': 'No button',
'cta_label': 'Open',
'cta_url': 'https://api.zarz.moe',
});
final missingLabel = RemoteAnnouncement.fromJson({
'id': 'notice',
'title': 'Notice',
'message': 'No button',
'cta_enabled': true,
'cta_url': 'https://example.test',
});
final enabledCta = RemoteAnnouncement.fromJson({
'id': 'notice',
'title': 'Notice',
'message': 'With button',
'cta_enabled': true,
'cta_label': 'Read More',
'cta_url': 'https://example.test',
});
expect(disabledCta.hasCta, isFalse);
expect(missingLabel.hasCta, isFalse);
expect(enabledCta.hasCta, isTrue);
expect(enabledCta.ctaLabel, 'Read More');
});
test('filters inactive announcements by window and app version', () {
final announcement = RemoteAnnouncement.fromJson({
'id': 'future',
'title': 'Future',
'message': 'Not yet',
'starts_at': '2026-06-01T00:00:00Z',
'min_version': '4.6.0',
});
expect(
announcement.isActive(
now: DateTime.utc(2026, 5, 11),
currentVersion: '4.5.1',
),
isFalse,
);
expect(
announcement.isActive(
now: DateTime.utc(2026, 6, 2),
currentVersion: '4.5.1',
),
isFalse,
);
expect(
announcement.isActive(
now: DateTime.utc(2026, 6, 2),
currentVersion: '4.6.0',
),
isTrue,
);
});
});
}