diff --git a/CHANGELOG.md b/CHANGELOG.md index ea574e3..b8c5d68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # manUp +## [9.4.0] + +- Allow custom background duration check timeout + ## [9.3.2] - Launch url in external browser diff --git a/README.md b/README.md index 1e9c988..a5f2ab3 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ Wrap your widget with `ManUpWidget` to automatically handle every thing. shouldShowAlert: () => true, onComplete: (bool isComplete) => print(isComplete), onError: (dynamic e) => print(e.toString()), + checkAfterBackgroundDuration: Duration(minutes: 5), // Optional, only re-check after the app has been in the background for a certain time child: Container()), ); } diff --git a/lib/src/ui/man_up_widget.dart b/lib/src/ui/man_up_widget.dart index a295661..7af984c 100644 --- a/lib/src/ui/man_up_widget.dart +++ b/lib/src/ui/man_up_widget.dart @@ -8,6 +8,12 @@ class ManUpWidget extends StatefulWidget { final void Function(dynamic e)? onError; final void Function(ManUpStatus status)? onStatusChanged; + /// After the app has been backgrounded for this duration, check for updates again. + final Duration checkAfterBackgroundDuration; + + @visibleForTesting + final DateTime Function() now; + ManUpWidget({ Key? key, required this.child, @@ -16,7 +22,10 @@ class ManUpWidget extends StatefulWidget { this.onComplete, this.onError, this.onStatusChanged, - }) : super(key: key); + this.checkAfterBackgroundDuration = Duration.zero, + @visibleForTesting DateTime Function()? now, + }) : now = now ?? DateTime.now, + super(key: key); @override _ManUpWidgetState createState() => _ManUpWidgetState(); @@ -30,6 +39,8 @@ class _ManUpWidgetState extends State WidgetsBindingObserver { ManUpStatus? alertDialogType; + DateTime? pausedAt; + @override void initState() { super.initState(); @@ -85,7 +96,14 @@ class _ManUpWidgetState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { - validateManUp(); + final inBackgroundFor = + pausedAt?.difference(widget.now()).abs() ?? Duration.zero; + if (inBackgroundFor >= widget.checkAfterBackgroundDuration) { + validateManUp(); + } + pausedAt = null; + } else if (state == AppLifecycleState.paused) { + pausedAt = widget.now(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 2da4f9d..5032e66 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: manup description: Mandatory update for Flutter Apps that prompts or forces app update by querying a hosted JSON file. -version: 9.3.2 +version: 9.4.0 homepage: https://github.com/NextFaze/flutter_manup environment: diff --git a/test/man_up_widget_test.dart b/test/man_up_widget_test.dart index 4126574..23fff22 100644 --- a/test/man_up_widget_test.dart +++ b/test/man_up_widget_test.dart @@ -7,7 +7,10 @@ void main() { const os = 'ios'; Future buildTestCase(WidgetTester tester, - {Metadata? metadata, String? version}) async { + {Metadata? metadata, + String? version, + Duration checkAfterBackgroundDuration = Duration.zero, + DateTime Function()? now}) async { final manUpService = MockManUpService( os: os, packageInfoProvider: @@ -17,7 +20,12 @@ void main() { manUpService.metadata = metadata; } await tester.pumpWidget(MaterialApp( - home: ManUpWidget(child: Container(), service: manUpService), + home: ManUpWidget( + child: Container(), + service: manUpService, + checkAfterBackgroundDuration: checkAfterBackgroundDuration, + now: now, + ), )); return manUpService; } @@ -122,6 +130,77 @@ void main() { tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); await tester.pumpAndSettle(); + expect(service.validateCallCount, 2); + expect(find.byType(AlertDialog), findsOneWidget); + expect( + find.text( + 'The app is currently in maintenance, please check again shortly.'), + findsOneWidget); + }); + + testWidgets( + 'skips resume re-check when background duration is below custom threshold', + (tester) async { + var now = DateTime(2020, 1, 1, 0, 0, 0); + final service = await buildTestCase( + tester, + checkAfterBackgroundDuration: Duration(minutes: 5), + now: () => now, + ); + + await tester.pumpAndSettle(); + expect(service.validateCallCount, 1); + expect(find.byType(AlertDialog), findsNothing); + + service.metadata = Metadata(data: { + os: { + "latest": "2.4.1", + "minimum": "2.1.0", + "url": "http://example.com/myAppUpdate", + "enabled": false + }, + }); + + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused); + await tester.pumpAndSettle(); + now = now.add(Duration(minutes: 4)); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + await tester.pumpAndSettle(); + + expect(service.validateCallCount, 1); + expect(find.byType(AlertDialog), findsNothing); + }); + + testWidgets( + 're-checks on resume when background duration meets custom threshold', + (tester) async { + var now = DateTime(2020, 1, 1, 0, 0, 0); + final service = await buildTestCase( + tester, + checkAfterBackgroundDuration: Duration(minutes: 5), + now: () => now, + ); + + await tester.pumpAndSettle(); + expect(service.validateCallCount, 1); + expect(find.byType(AlertDialog), findsNothing); + + service.metadata = Metadata(data: { + os: { + "latest": "2.4.1", + "minimum": "2.1.0", + "url": "http://example.com/myAppUpdate", + "enabled": false + }, + }); + + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused); + await tester.pumpAndSettle(); + now = now.add(Duration(minutes: 6)); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + await tester.pumpAndSettle(); + + expect(service.validateCallCount, 2); expect(find.byType(AlertDialog), findsOneWidget); expect( find.text( @@ -188,6 +267,13 @@ class MockManUpService extends ManUpService { MockManUpService({super.os, super.packageInfoProvider}); Metadata metadata = Metadata(data: {}); + int validateCallCount = 0; + + @override + Future validate([Metadata? metadata]) { + validateCallCount += 1; + return super.validate(metadata); + } @override Future getMetadata() async => metadata;