From 11504ceaae3b9df24983f0fcde7e5ccf38e2ee09 Mon Sep 17 00:00:00 2001 From: gonfff Date: Tue, 13 Jan 2026 11:12:07 +0300 Subject: [PATCH 1/2] Recount subs on resume app --- .../screens/subscriptions_screen.dart | 17 ++++++++++--- .../viewmodels/subscriptions_view_model.dart | 7 ++++++ .../subscriptions_view_model_test.dart | 25 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/lib/presentation/screens/subscriptions_screen.dart b/lib/presentation/screens/subscriptions_screen.dart index f7fdc97..4438e79 100644 --- a/lib/presentation/screens/subscriptions_screen.dart +++ b/lib/presentation/screens/subscriptions_screen.dart @@ -1,11 +1,12 @@ -import 'package:flutter/cupertino.dart'; +import 'dart:async'; +import 'package:flutter/cupertino.dart'; import 'package:subctrl/application/app_dependencies.dart'; +import 'package:subctrl/domain/entities/notification_reminder_option.dart'; import 'package:subctrl/domain/entities/subscription.dart'; import 'package:subctrl/presentation/l10n/app_localizations.dart'; import 'package:subctrl/presentation/theme/app_theme.dart'; import 'package:subctrl/presentation/theme/theme_preference.dart'; -import 'package:subctrl/domain/entities/notification_reminder_option.dart'; import 'package:subctrl/presentation/types/settings_callbacks.dart'; import 'package:subctrl/presentation/viewmodels/subscriptions_view_model.dart'; import 'package:subctrl/presentation/widgets/add_subscription_sheet.dart'; @@ -49,7 +50,8 @@ class SubscriptionsScreen extends StatefulWidget { State createState() => _SubscriptionsScreenState(); } -class _SubscriptionsScreenState extends State { +class _SubscriptionsScreenState extends State + with WidgetsBindingObserver { late final SubscriptionsViewModel _viewModel; late final ScrollController _scrollController; final TextEditingController _searchController = TextEditingController(); @@ -57,6 +59,7 @@ class _SubscriptionsScreenState extends State { @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); _scrollController = ScrollController(); _viewModel = SubscriptionsViewModel( watchSubscriptionsUseCase: widget.dependencies.watchSubscriptionsUseCase, @@ -95,9 +98,17 @@ class _SubscriptionsScreenState extends State { _scrollController.dispose(); _searchController.dispose(); _viewModel.dispose(); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + unawaited(_viewModel.refreshOverdueNextPayments()); + } + } + @override void didUpdateWidget(covariant SubscriptionsScreen oldWidget) { super.didUpdateWidget(oldWidget); diff --git a/lib/presentation/viewmodels/subscriptions_view_model.dart b/lib/presentation/viewmodels/subscriptions_view_model.dart index 847937e..0001d73 100644 --- a/lib/presentation/viewmodels/subscriptions_view_model.dart +++ b/lib/presentation/viewmodels/subscriptions_view_model.dart @@ -217,6 +217,13 @@ class SubscriptionsViewModel extends ChangeNotifier { return _refreshCurrencyRatesForSubscriptions(); } + Future refreshOverdueNextPayments() async { + if (_isLoadingSubscriptions || _subscriptions.isEmpty) { + return; + } + await _refreshOverdueNextPayments(_subscriptions); + } + Future ensureCurrenciesLoaded() async { if (!_isLoadingCurrencies && _currencies.isNotEmpty) { return; diff --git a/test/presentation/viewmodels/subscriptions_view_model_test.dart b/test/presentation/viewmodels/subscriptions_view_model_test.dart index 36d3a4f..f5d69d0 100644 --- a/test/presentation/viewmodels/subscriptions_view_model_test.dart +++ b/test/presentation/viewmodels/subscriptions_view_model_test.dart @@ -280,6 +280,31 @@ void main() { ).called(1); }); + test('refreshOverdueNextPayments uses current subscriptions', () async { + final subscription = Subscription( + id: 2, + name: 'HBO', + amount: 12, + currency: 'usd', + cycle: BillingCycle.monthly, + purchaseDate: DateTime(2024, 1, 1), + ); + subscriptionsController.add([subscription]); + tagsController.add(const []); + currenciesController.add(const []); + ratesController.add(const []); + await Future.delayed(Duration.zero); + + clearInteractions(refreshOverdueNextPaymentsUseCase); + await viewModel.refreshOverdueNextPayments(); + + verify( + () => refreshOverdueNextPaymentsUseCase( + any(that: predicate>((subs) => subs.length == 1)), + ), + ).called(1); + }); + test('updateBaseCurrencyCode re-listens to currency rates stream', () async { viewModel.updateBaseCurrencyCode('eur'); await Future.delayed(Duration.zero); From af1b90cc7a78c18ff7287267e62f8dfd8b0ec0ef Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:59:29 +0300 Subject: [PATCH 2/2] Add edge case tests for refreshOverdueNextPayments guards (#13) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gonfff <42554983+gonfff@users.noreply.github.com> --- .../subscriptions_view_model_test.dart | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/presentation/viewmodels/subscriptions_view_model_test.dart b/test/presentation/viewmodels/subscriptions_view_model_test.dart index f5d69d0..0d40be6 100644 --- a/test/presentation/viewmodels/subscriptions_view_model_test.dart +++ b/test/presentation/viewmodels/subscriptions_view_model_test.dart @@ -305,6 +305,32 @@ void main() { ).called(1); }); + test( + 'refreshOverdueNextPayments does nothing when subscriptions are empty', + () async { + subscriptionsController.add(const []); + tagsController.add(const []); + currenciesController.add(const []); + ratesController.add(const []); + await Future.delayed(Duration.zero); + + clearInteractions(refreshOverdueNextPaymentsUseCase); + await viewModel.refreshOverdueNextPayments(); + + verifyNever(() => refreshOverdueNextPaymentsUseCase(any())); + }, + ); + + test( + 'refreshOverdueNextPayments does nothing when subscriptions are loading', + () async { + clearInteractions(refreshOverdueNextPaymentsUseCase); + await viewModel.refreshOverdueNextPayments(); + + verifyNever(() => refreshOverdueNextPaymentsUseCase(any())); + }, + ); + test('updateBaseCurrencyCode re-listens to currency rates stream', () async { viewModel.updateBaseCurrencyCode('eur'); await Future.delayed(Duration.zero);