From 8e626e53d163a473322f6da61d03389acc445cde Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Fri, 19 Jan 2024 08:16:49 -0500 Subject: [PATCH 01/15] First pass at moving the code that downloads version details from the app stores and Appcast to store controllers. --- README.md | 2 +- example/analysis_options.yaml | 49 +- example/lib/main.dart | 9 +- ...alert-theme.dart => main_alert_theme.dart} | 6 +- .../{main-appcast.dart => main_appcast.dart} | 12 +- .../lib/{main-card.dart => main_card.dart} | 13 +- ...n-card-theme.dart => main_card_theme.dart} | 13 +- ...ain-cupertino.dart => main_cupertino.dart} | 12 +- ...stom-alert.dart => main_custom_alert.dart} | 20 +- ...custom-card.dart => main_custom_card.dart} | 17 +- example/lib/main_dialog_key.dart | 15 +- .../{main-driver.dart => main_driver.dart} | 16 +- ...{main-gorouter.dart => main_gorouter.dart} | 6 +- example/lib/main_localized_rtl.dart | 18 +- .../lib/{main-macos.dart => main_macos.dart} | 10 +- ...{main-messages.dart => main_messages.dart} | 88 ++- ...version.dart => main_min_app_version.dart} | 14 +- example/lib/main_multiple.dart | 6 +- ...{main-stateful.dart => main_stateful.dart} | 13 +- example/lib/main_subclass.dart | 15 +- example/pubspec.yaml | 9 +- example/test/driver_test/driver.dart | 2 +- lib/src/alert_style_widget.dart | 4 +- lib/src/play_store_search_api.dart | 1 + lib/src/upgrade_alert.dart | 2 +- lib/src/upgrade_os.dart | 36 ++ lib/src/upgrader.dart | 558 +++++++++++++----- test/upgrader_test.dart | 78 +-- 28 files changed, 620 insertions(+), 424 deletions(-) rename example/lib/{main-alert-theme.dart => main_alert_theme.dart} (89%) rename example/lib/{main-appcast.dart => main_appcast.dart} (78%) rename example/lib/{main-card.dart => main_card.dart} (69%) rename example/lib/{main-card-theme.dart => main_card_theme.dart} (73%) rename example/lib/{main-cupertino.dart => main_cupertino.dart} (61%) rename example/lib/{main-custom-alert.dart => main_custom_alert.dart} (78%) rename example/lib/{main-custom-card.dart => main_custom_card.dart} (78%) rename example/lib/{main-driver.dart => main_driver.dart} (81%) rename example/lib/{main-gorouter.dart => main_gorouter.dart} (90%) rename example/lib/{main-macos.dart => main_macos.dart} (75%) rename example/lib/{main-messages.dart => main_messages.dart} (63%) rename example/lib/{main-min-app-version.dart => main_min_app_version.dart} (64%) rename example/lib/{main-stateful.dart => main_stateful.dart} (63%) diff --git a/README.md b/README.md index 6c6be291..6d1371ec 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Tapping the UPDATE NOW button takes the user to the App Store (iOS) or Google Pl Just wrap your home widget in the `UpgradeAlert` widget, and it will handle the rest. ```dart class MyApp extends StatelessWidget { - const MyApp({Key key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 0c60ac16..387755d2 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,47 +1,6 @@ +include: package:flutter_lints/flutter.yaml + linter: rules: - - always_declare_return_types - - always_require_non_null_named_parameters - - annotate_overrides - - avoid_empty_else - - avoid_init_to_null - - avoid_null_checks_in_equality_operators - - avoid_relative_lib_imports - - avoid_return_types_on_setters - - avoid_shadowing_type_parameters - - avoid_types_as_parameter_names - - camel_case_extensions - - curly_braces_in_flow_control_structures - - empty_catches - - empty_constructor_bodies - - library_names - - library_prefixes - - no_duplicate_case_values - - null_closures - - omit_local_variable_types - - prefer_adjacent_string_concatenation - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_contains - - prefer_equal_for_default_values - - prefer_final_fields - - prefer_for_elements_to_map_fromIterable - - prefer_generic_function_type_aliases - - prefer_if_null_operators - - prefer_is_empty - - prefer_is_not_empty - - prefer_iterable_whereType - - prefer_single_quotes - - prefer_spread_collections - - recursive_getters - - slash_for_doc_comments - - type_init_formals - - unawaited_futures - - unnecessary_const - - unnecessary_new - - unnecessary_null_in_if_null_operators - - unnecessary_this - - unrelated_type_equality_checks - - use_function_type_syntax_for_parameters - - use_rethrow_when_possible - - valid_regexps \ No newline at end of file + avoid_function_literals_in_foreach_calls: false + avoid_print: false diff --git a/example/lib/main.dart b/example/lib/main.dart index abb11ecf..253e667c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -16,7 +16,7 @@ void main() async { // On iOS, the default behavior will be to use the App Store version of // the app, so update the Bundle Identifier in example/ios/Runner with a // valid identifier already in the App Store. - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { @@ -27,10 +27,11 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Upgrader Example', home: UpgradeAlert( + upgrader: Upgrader(debugLogging: true), child: Scaffold( - appBar: AppBar(title: Text('Upgrader Example')), - body: Center(child: Text('Checking...')), - )), + appBar: AppBar(title: const Text('Upgrader Example')), + body: const Center(child: Text('Checking...')), + )), ); } } diff --git a/example/lib/main-alert-theme.dart b/example/lib/main_alert_theme.dart similarity index 89% rename from example/lib/main-alert-theme.dart rename to example/lib/main_alert_theme.dart index 54587a3f..4f1009f9 100644 --- a/example/lib/main-alert-theme.dart +++ b/example/lib/main_alert_theme.dart @@ -23,7 +23,7 @@ class MyApp extends StatelessWidget { final dark = ThemeData.dark(useMaterial3: true); final light = ThemeData( - dialogTheme: DialogTheme( + dialogTheme: const DialogTheme( titleTextStyle: TextStyle(color: Colors.red, fontSize: 48), contentTextStyle: TextStyle(color: Colors.green, fontSize: 18), ), @@ -42,8 +42,8 @@ class MyApp extends StatelessWidget { title: 'Upgrader Example', home: UpgradeAlert( child: Scaffold( - appBar: AppBar(title: Text('Upgrader Alert Theme Example')), - body: Center(child: Text('Checking...')), + appBar: AppBar(title: const Text('Upgrader Alert Theme Example')), + body: const Center(child: Text('Checking...')), )), theme: light, darkTheme: dark, diff --git a/example/lib/main-appcast.dart b/example/lib/main_appcast.dart similarity index 78% rename from example/lib/main-appcast.dart rename to example/lib/main_appcast.dart index 236ec972..7cc4dc09 100644 --- a/example/lib/main-appcast.dart +++ b/example/lib/main_appcast.dart @@ -19,23 +19,25 @@ void main() async { } class MyApp extends StatelessWidget { - MyApp({Key? key}) : super(key: key); + MyApp({super.key}); static const appcastURL = 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; final upgrader = Upgrader( - appcastConfig: - AppcastConfiguration(url: appcastURL, supportedOS: ['android'])); + storeController: UpgraderStoreController( + onAndroid: () => UpgraderAppcastStore(appcastURL: appcastURL), + ), + ); @override Widget build(BuildContext context) { return MaterialApp( title: 'Upgrader Example', home: Scaffold( - appBar: AppBar(title: Text('Upgrader Appcast Example')), + appBar: AppBar(title: const Text('Upgrader Appcast Example')), body: UpgradeAlert( upgrader: upgrader, - child: Center(child: Text('Checking...')), + child: const Center(child: Text('Checking...')), )), ); } diff --git a/example/lib/main-card.dart b/example/lib/main_card.dart similarity index 69% rename from example/lib/main-card.dart rename to example/lib/main_card.dart index 08b4b300..01d28117 100644 --- a/example/lib/main-card.dart +++ b/example/lib/main_card.dart @@ -11,12 +11,7 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, the default behavior will be to use the Google Play Store - // version of the app. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { @@ -27,9 +22,9 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Upgrader Card Example', home: Scaffold( - appBar: AppBar(title: Text('Upgrader Card Example')), + appBar: AppBar(title: const Text('Upgrader Card Example')), body: Container( - margin: EdgeInsets.only(left: 12.0, right: 12.0), + margin: const EdgeInsets.only(left: 12.0, right: 12.0), child: SingleChildScrollView( child: Column( children: [ @@ -46,7 +41,7 @@ class MyApp extends StatelessWidget { ); } - Widget get _simpleCard => Card( + Widget get _simpleCard => const Card( child: SizedBox( width: 200, height: 50, diff --git a/example/lib/main-card-theme.dart b/example/lib/main_card_theme.dart similarity index 73% rename from example/lib/main-card-theme.dart rename to example/lib/main_card_theme.dart index b6cf347b..739f39d7 100644 --- a/example/lib/main-card-theme.dart +++ b/example/lib/main_card_theme.dart @@ -9,11 +9,6 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, the default behavior will be to use the Google Play Store - // version of the app. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. runApp(MyApp()); } @@ -23,7 +18,7 @@ class MyApp extends StatelessWidget { final dark = ThemeData.dark(useMaterial3: true); final light = ThemeData( - cardTheme: CardTheme(color: Colors.greenAccent), + cardTheme: const CardTheme(color: Colors.greenAccent), // Change the text buttons. textButtonTheme: const TextButtonThemeData( style: ButtonStyle( @@ -38,9 +33,9 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Upgrader Card Example', home: Scaffold( - appBar: AppBar(title: Text('Upgrader Card Theme Example')), + appBar: AppBar(title: const Text('Upgrader Card Theme Example')), body: Container( - margin: EdgeInsets.only(left: 12.0, right: 12.0), + margin: const EdgeInsets.only(left: 12.0, right: 12.0), child: SingleChildScrollView( child: Column( children: [ @@ -59,7 +54,7 @@ class MyApp extends StatelessWidget { ); } - Widget get _simpleCard => Card( + Widget get _simpleCard => const Card( child: SizedBox( width: 200, height: 50, diff --git a/example/lib/main-cupertino.dart b/example/lib/main_cupertino.dart similarity index 61% rename from example/lib/main-cupertino.dart rename to example/lib/main_cupertino.dart index 5546804b..10da6511 100644 --- a/example/lib/main-cupertino.dart +++ b/example/lib/main_cupertino.dart @@ -11,25 +11,21 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, setup the Appcast. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { - MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Upgrader Example', home: Scaffold( - appBar: AppBar(title: Text('Upgrader Cupertino Example')), + appBar: AppBar(title: const Text('Upgrader Cupertino Example')), body: UpgradeAlert( dialogStyle: UpgradeDialogStyle.cupertino, - child: Center(child: Text('Checking...')), + child: const Center(child: Text('Checking...')), ), ), ); diff --git a/example/lib/main-custom-alert.dart b/example/lib/main_custom_alert.dart similarity index 78% rename from example/lib/main-custom-alert.dart rename to example/lib/main_custom_alert.dart index 8aa45fa8..d3782aef 100644 --- a/example/lib/main-custom-alert.dart +++ b/example/lib/main_custom_alert.dart @@ -9,11 +9,6 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, the default behavior will be to use the Google Play Store - // version of the app. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. runApp(MyApp()); } @@ -29,8 +24,8 @@ class MyApp extends StatelessWidget { home: MyUpgradeAlert( upgrader: upgrader, child: Scaffold( - appBar: AppBar(title: Text('Upgrader Custom Alert Example')), - body: Center(child: Text('Checking...')), + appBar: AppBar(title: const Text('Upgrader Custom Alert Example')), + body: const Center(child: Text('Checking...')), )), ); } @@ -39,23 +34,18 @@ class MyApp extends StatelessWidget { class MyUpgrader extends Upgrader { MyUpgrader({super.debugLogging}); - @override - bool isTooSoon() { - return super.isTooSoon(); - } - @override bool isUpdateAvailable() { - final appStoreVersion = currentAppStoreVersion; + final storeVersion = currentAppStoreVersion; final installedVersion = currentInstalledVersion; - print('appStoreVersion=$appStoreVersion'); + print('storeVersion=$storeVersion'); print('installedVersion=$installedVersion'); return super.isUpdateAvailable(); } } class MyUpgradeAlert extends UpgradeAlert { - MyUpgradeAlert({super.upgrader, super.child}); + MyUpgradeAlert({super.key, super.upgrader, super.child}); /// Override the [createState] method to provide a custom class /// with overridden methods. diff --git a/example/lib/main-custom-card.dart b/example/lib/main_custom_card.dart similarity index 78% rename from example/lib/main-custom-card.dart rename to example/lib/main_custom_card.dart index 57289a65..3c6612e9 100644 --- a/example/lib/main-custom-card.dart +++ b/example/lib/main_custom_card.dart @@ -9,12 +9,7 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, the default behavior will be to use the Google Play Store - // version of the app. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { @@ -25,9 +20,9 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Upgrader Example', home: Scaffold( - appBar: AppBar(title: Text('Upgrader Custom Card Example')), + appBar: AppBar(title: const Text('Upgrader Custom Card Example')), body: Container( - margin: EdgeInsets.only(left: 12.0, right: 12.0), + margin: const EdgeInsets.only(left: 12.0, right: 12.0), child: SingleChildScrollView( child: Column( children: [ @@ -44,7 +39,7 @@ class MyApp extends StatelessWidget { ); } - Widget get _simpleCard => Card( + Widget get _simpleCard => const Card( child: SizedBox( width: 200, height: 50, @@ -54,7 +49,7 @@ class MyApp extends StatelessWidget { } class MyUpgradeCard extends UpgradeCard { - MyUpgradeCard({super.upgrader}); + MyUpgradeCard({super.key, super.upgrader}); /// Override the [createState] method to provide a custom class /// with overridden methods. @@ -80,7 +75,7 @@ class MyUpgradeCardState extends UpgradeCardState { }, ), ], - content: Text(''), + content: const Text(''), title: Text(title ?? ''), ), ); diff --git a/example/lib/main_dialog_key.dart b/example/lib/main_dialog_key.dart index 4f4418ce..afbb0202 100644 --- a/example/lib/main_dialog_key.dart +++ b/example/lib/main_dialog_key.dart @@ -13,13 +13,12 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - final log = - () => print('$dialogKey mounted=${dialogKey.currentContext?.mounted}'); - unawaited(Future.delayed(Duration(seconds: 0)).then((value) => log())); - unawaited(Future.delayed(Duration(seconds: 3)).then((value) => log())); - unawaited(Future.delayed(Duration(seconds: 4)).then((value) => log())); + log() => print('$dialogKey mounted=${dialogKey.currentContext?.mounted}'); + unawaited(Future.delayed(const Duration(seconds: 0)).then((value) => log())); + unawaited(Future.delayed(const Duration(seconds: 3)).then((value) => log())); + unawaited(Future.delayed(const Duration(seconds: 4)).then((value) => log())); - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { @@ -32,8 +31,8 @@ class MyApp extends StatelessWidget { home: UpgradeAlert( dialogKey: dialogKey, child: Scaffold( - appBar: AppBar(title: Text('Upgrader Example')), - body: Center(child: Text('Checking...')), + appBar: AppBar(title: const Text('Upgrader Example')), + body: const Center(child: Text('Checking...')), )), ); } diff --git a/example/lib/main-driver.dart b/example/lib/main_driver.dart similarity index 81% rename from example/lib/main-driver.dart rename to example/lib/main_driver.dart index a01bc446..983fb201 100644 --- a/example/lib/main-driver.dart +++ b/example/lib/main_driver.dart @@ -9,7 +9,7 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatefulWidget { @@ -26,27 +26,27 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { final scaffold = Scaffold( - appBar: AppBar(title: Text('Upgrader Driver App')), + appBar: AppBar(title: const Text('Upgrader Driver App')), body: Center( child: Column( children: [ - SizedBox(height: 32.0), + const SizedBox(height: 32.0), ElevatedButton( onPressed: () async { await Upgrader.clearSavedSettings(); _upgrader = Upgrader(debugLogging: true); setState(() => _testState = 1); }, - child: Text('Dialog Alert'), + child: const Text('Dialog Alert'), ), - SizedBox(height: 16.0), + const SizedBox(height: 16.0), ElevatedButton( onPressed: () async { await Upgrader.clearSavedSettings(); _upgrader = Upgrader(debugLogging: true); setState(() => _testState = 2); }, - child: Text('Dialog Alert - Cupertino'), + child: const Text('Dialog Alert - Cupertino'), ), ], )), @@ -59,11 +59,11 @@ class _MyAppState extends State { break; case 1: content = UpgradeAlert( - key: Key('ua_1'), upgrader: _upgrader, child: scaffold); + key: const Key('ua_1'), upgrader: _upgrader, child: scaffold); break; case 2: content = UpgradeAlert( - key: Key('ua_2'), + key: const Key('ua_2'), upgrader: _upgrader, dialogStyle: UpgradeDialogStyle.cupertino, child: scaffold); diff --git a/example/lib/main-gorouter.dart b/example/lib/main_gorouter.dart similarity index 90% rename from example/lib/main-gorouter.dart rename to example/lib/main_gorouter.dart index c451ee09..c27114de 100644 --- a/example/lib/main-gorouter.dart +++ b/example/lib/main_gorouter.dart @@ -10,7 +10,7 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - runApp(MyApp()); + runApp(const MyApp()); } final routerConfig = GoRouter( @@ -38,7 +38,7 @@ class MyApp extends StatelessWidget { builder: (context, child) { return UpgradeAlert( navigatorKey: routerConfig.routerDelegate.navigatorKey, - child: child ?? Text('child'), + child: child ?? const Text('child'), ); }, ); @@ -53,7 +53,7 @@ class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('Upgrader GoRouter Example')), + appBar: AppBar(title: const Text('Upgrader GoRouter Example')), body: Center(child: Text('Checking... $title')), ); } diff --git a/example/lib/main_localized_rtl.dart b/example/lib/main_localized_rtl.dart index c963951c..cbf42f4f 100644 --- a/example/lib/main_localized_rtl.dart +++ b/example/lib/main_localized_rtl.dart @@ -10,28 +10,30 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({super.key}); + @override Widget build(BuildContext context) { return MaterialApp( - locale: Locale('ar'), // Arabic language shows right to left. - localizationsDelegates: [ + locale: const Locale('ar'), // Arabic language shows right to left. + localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: [ - const Locale('ar', ''), // Arabic, no country code - const Locale('he', ''), // Hebrew, no country code + supportedLocales: const [ + Locale('ar', ''), // Arabic, no country code + Locale('he', ''), // Hebrew, no country code ], title: 'Upgrader Left to Right Example', home: UpgradeAlert( child: Scaffold( - appBar: AppBar(title: Text('Upgrader Left to Right Example')), - body: Center(child: Text('Checking...')), + appBar: AppBar(title: const Text('Upgrader Left to Right Example')), + body: const Center(child: Text('Checking...')), )), ); } diff --git a/example/lib/main-macos.dart b/example/lib/main_macos.dart similarity index 75% rename from example/lib/main-macos.dart rename to example/lib/main_macos.dart index d6271b46..7d3c7795 100644 --- a/example/lib/main-macos.dart +++ b/example/lib/main_macos.dart @@ -13,13 +13,13 @@ void main() async { } class MyApp extends StatelessWidget { - MyApp({Key? key}) : super(key: key); + MyApp({super.key}); static const appcastURL = 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast_macos.xml'; final upgrader = Upgrader( - appcastConfig: - AppcastConfiguration(url: appcastURL, supportedOS: ['macos']), + storeController: UpgraderStoreController( + onMacOS: () => UpgraderAppcastStore(appcastURL: appcastURL)), debugLogging: true, ); @@ -30,8 +30,8 @@ class MyApp extends StatelessWidget { home: UpgradeAlert( upgrader: upgrader, child: Scaffold( - appBar: AppBar(title: Text('Upgrader Example')), - body: Center(child: Text('Checking...')), + appBar: AppBar(title: const Text('Upgrader Example')), + body: const Center(child: Text('Checking...')), )), ); } diff --git a/example/lib/main-messages.dart b/example/lib/main_messages.dart similarity index 63% rename from example/lib/main-messages.dart rename to example/lib/main_messages.dart index 44567a86..60d31e9c 100644 --- a/example/lib/main-messages.dart +++ b/example/lib/main_messages.dart @@ -13,15 +13,11 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, setup the Appcast. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { - MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { @@ -29,47 +25,47 @@ class MyApp extends StatelessWidget { onGenerateTitle: (BuildContext context) => DemoLocalizations.of(context).title, home: DemoApp(), - localizationsDelegates: [ - const DemoLocalizationsDelegate(), + localizationsDelegates: const [ + DemoLocalizationsDelegate(), GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: [ - const Locale('en', ''), // English, no country code - const Locale('ar', ''), // Arabic, no country code - const Locale('bn', ''), // Bengali, no country code - const Locale('da', ''), // Danish, no country code - const Locale('es', ''), // Spanish, no country code - const Locale('fa', ''), // Persian, no country code - const Locale('fil', ''), // Filipino, no country code - const Locale('fr', ''), // French, no country code - const Locale('de', ''), // German, no country code - const Locale('el', ''), // Greek, no country code - const Locale('he', ''), // Hebrew, no country code - const Locale('hi', ''), // Hindi, no country code - const Locale('ht', ''), // Haitian Creole, no country code - const Locale('hu', ''), // Hungarian, no country code - const Locale('id', ''), // Indonesian, no country code - const Locale('it', ''), // Italian, no country code - const Locale('ja', ''), // Japanese, no country code - const Locale('kk', ''), // Kazakh, no country code - const Locale('km', ''), // Khmer, no country code - const Locale('ko', ''), // Korean, no country code - const Locale('lt', ''), // Lithuanian, no country code - const Locale('mn', ''), // Mongolian, no country code - const Locale('nb', ''), // Norwegian, no country code - const Locale('nl', ''), // Dutch, no country code - const Locale('pt', ''), // Portuguese, no country code - const Locale('pl', ''), // Polish, no country code - const Locale('ru', ''), // Russian, no country code - const Locale('sv', ''), // Swedish, no country code - const Locale('ta', ''), // Tamil, no country code - const Locale('te', ''), // Telugu, no country code - const Locale('tr', ''), // Turkish, no country code - const Locale('uk', ''), // Ukrainian, no country code - const Locale('vi', ''), // Vietnamese, no country code - const Locale('zh', ''), // Chinese, no country code + supportedLocales: const [ + Locale('en', ''), // English, no country code + Locale('ar', ''), // Arabic, no country code + Locale('bn', ''), // Bengali, no country code + Locale('da', ''), // Danish, no country code + Locale('es', ''), // Spanish, no country code + Locale('fa', ''), // Persian, no country code + Locale('fil', ''), // Filipino, no country code + Locale('fr', ''), // French, no country code + Locale('de', ''), // German, no country code + Locale('el', ''), // Greek, no country code + Locale('he', ''), // Hebrew, no country code + Locale('hi', ''), // Hindi, no country code + Locale('ht', ''), // Haitian Creole, no country code + Locale('hu', ''), // Hungarian, no country code + Locale('id', ''), // Indonesian, no country code + Locale('it', ''), // Italian, no country code + Locale('ja', ''), // Japanese, no country code + Locale('kk', ''), // Kazakh, no country code + Locale('km', ''), // Khmer, no country code + Locale('ko', ''), // Korean, no country code + Locale('lt', ''), // Lithuanian, no country code + Locale('mn', ''), // Mongolian, no country code + Locale('nb', ''), // Norwegian, no country code + Locale('nl', ''), // Dutch, no country code + Locale('pt', ''), // Portuguese, no country code + Locale('pl', ''), // Polish, no country code + Locale('ru', ''), // Russian, no country code + Locale('sv', ''), // Swedish, no country code + Locale('ta', ''), // Tamil, no country code + Locale('te', ''), // Telugu, no country code + Locale('tr', ''), // Turkish, no country code + Locale('uk', ''), // Ukrainian, no country code + Locale('vi', ''), // Vietnamese, no country code + Locale('zh', ''), // Chinese, no country code ], ); } @@ -79,12 +75,14 @@ class DemoApp extends StatelessWidget { static const appcastURL = 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; final upgrader = Upgrader( - appcastConfig: - AppcastConfiguration(url: appcastURL, supportedOS: ['android']), + storeController: UpgraderStoreController( + onAndroid: () => UpgraderAppcastStore(appcastURL: appcastURL)), debugLogging: true, messages: MyUpgraderMessages(code: 'es'), ); + DemoApp({super.key}); + @override Widget build(BuildContext context) { return Scaffold( diff --git a/example/lib/main-min-app-version.dart b/example/lib/main_min_app_version.dart similarity index 64% rename from example/lib/main-min-app-version.dart rename to example/lib/main_min_app_version.dart index 7f47fb94..c9aa7e30 100644 --- a/example/lib/main-min-app-version.dart +++ b/example/lib/main_min_app_version.dart @@ -11,21 +11,17 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, setup the Appcast. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. runApp(MyApp()); } class MyApp extends StatelessWidget { - MyApp({Key? key}) : super(key: key); + MyApp({super.key}); static const appcastURL = 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; final upgrader = Upgrader( - appcastConfig: - AppcastConfiguration(url: appcastURL, supportedOS: ['android']), + storeController: UpgraderStoreController( + onAndroid: () => UpgraderAppcastStore(appcastURL: appcastURL)), debugLogging: true, minAppVersion: '1.1.0', ); @@ -35,10 +31,10 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Upgrader Example', home: Scaffold( - appBar: AppBar(title: Text('Upgrader Example')), + appBar: AppBar(title: const Text('Upgrader Example')), body: UpgradeAlert( upgrader: upgrader, - child: Center(child: Text('Checking...')), + child: const Center(child: Text('Checking...')), )), ); } diff --git a/example/lib/main_multiple.dart b/example/lib/main_multiple.dart index e803e6a4..5c186cc0 100644 --- a/example/lib/main_multiple.dart +++ b/example/lib/main_multiple.dart @@ -9,7 +9,7 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { @@ -21,8 +21,8 @@ class MyApp extends StatelessWidget { title: 'Upgrader Example - Multiple', home: UpgradeAlert( child: Scaffold( - appBar: AppBar(title: Text('Upgrader Example - Multiple')), - body: Center(child: UpgradeAlert(child: Text('Checking...'))), + appBar: AppBar(title: const Text('Upgrader Example - Multiple')), + body: Center(child: UpgradeAlert(child: const Text('Checking...'))), )), ); } diff --git a/example/lib/main-stateful.dart b/example/lib/main_stateful.dart similarity index 63% rename from example/lib/main-stateful.dart rename to example/lib/main_stateful.dart index 20ece52c..a9691893 100644 --- a/example/lib/main-stateful.dart +++ b/example/lib/main_stateful.dart @@ -11,12 +11,7 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, the default behavior will be to use the Google Play Store - // version of the app. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatefulWidget { @@ -30,7 +25,7 @@ class _MyAppState extends State { @override void initState() { super.initState(); - Future.delayed(Duration()).then((value) { + Future.delayed(const Duration()).then((value) { setState(() {}); }); } @@ -41,8 +36,8 @@ class _MyAppState extends State { title: 'Upgrader StatefulWidget Example', home: UpgradeAlert( child: Scaffold( - appBar: AppBar(title: Text('Upgrader StatefulWidget Example')), - body: Center(child: Text('Checking...')), + appBar: AppBar(title: const Text('Upgrader StatefulWidget Example')), + body: const Center(child: Text('Checking...')), )), ); } diff --git a/example/lib/main_subclass.dart b/example/lib/main_subclass.dart index f0f9f56c..cffa3457 100644 --- a/example/lib/main_subclass.dart +++ b/example/lib/main_subclass.dart @@ -11,28 +11,23 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, the default behavior will be to use the Google Play Store - // version of the app. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. runApp(MyApp()); } class MyApp extends StatelessWidget { - MyApp({Key? key}) : super(key: key); + MyApp({super.key}); - final upgrader = MyUpgrader(); + final _upgrader = MyUpgrader(); @override Widget build(BuildContext context) { return MaterialApp( title: 'Upgrader Subclass Example', home: Scaffold( - appBar: AppBar(title: Text('Upgrader Subclass Example')), + appBar: AppBar(title: const Text('Upgrader Subclass Example')), body: UpgradeAlert( - upgrader: upgrader, - child: Center(child: Text('Checking...')), + upgrader: _upgrader, + child: const Center(child: Text('Checking...')), )), ); } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 7e2388e6..35f2a143 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -12,17 +12,20 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - upgrader: - path: ../ go_router: ^7.1.1 - http: ^1.0.0 + path: ^1.8.3 + + upgrader: + path: ../ dev_dependencies: flutter_driver: sdk: flutter + flutter_lints: ^2.0.3 + flutter_test: sdk: flutter diff --git a/example/test/driver_test/driver.dart b/example/test/driver_test/driver.dart index 25b11c93..5a0c8fa7 100644 --- a/example/test/driver_test/driver.dart +++ b/example/test/driver_test/driver.dart @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:upgrader_example/main-driver.dart' as app; +import 'package:upgrader_example/main_driver.dart' as app; import 'package:flutter_driver/driver_extension.dart'; void main() { diff --git a/lib/src/alert_style_widget.dart b/lib/src/alert_style_widget.dart index ea047acc..d681c244 100644 --- a/lib/src/alert_style_widget.dart +++ b/lib/src/alert_style_widget.dart @@ -33,11 +33,11 @@ class AlertStyleWidget extends StatelessWidget { final Widget? title; const AlertStyleWidget({ - Key? key, + super.key, required this.content, required this.actions, this.title, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/src/play_store_search_api.dart b/lib/src/play_store_search_api.dart index f83e46d6..8a49cb4e 100644 --- a/lib/src/play_store_search_api.dart +++ b/lib/src/play_store_search_api.dart @@ -58,6 +58,7 @@ class PlayStoreSearchAPI { } } + /// Create a URL that points to the Play Store details for an app. String? lookupURLById(String id, {String? country = 'US', String? language = 'en', diff --git a/lib/src/upgrade_alert.dart b/lib/src/upgrade_alert.dart index ec64db7d..ea73e611 100644 --- a/lib/src/upgrade_alert.dart +++ b/lib/src/upgrade_alert.dart @@ -134,7 +134,7 @@ class UpgradeAlertState extends State { void checkVersion({required BuildContext context}) { final shouldDisplay = widget.upgrader.shouldDisplayUpgrade(); if (widget.upgrader.debugLogging) { - print('upgrader: shouldDisplayReleaseNotes: shouldDisplayReleaseNotes'); + print('upgrader: shouldDisplayReleaseNotes: $shouldDisplayReleaseNotes'); } if (shouldDisplay) { displayed = true; diff --git a/lib/src/upgrade_os.dart b/lib/src/upgrade_os.dart index 8801e706..3e13cf4e 100644 --- a/lib/src/upgrade_os.dart +++ b/lib/src/upgrade_os.dart @@ -5,9 +5,20 @@ import "package:os_detect/os_detect.dart" as platform; import 'package:flutter/foundation.dart'; +enum UpgraderOSType { + android, + fuchsia, + ios, + linux, + macos, + web, + windows, +} + /// A class that indicates which OS this code is running on. class UpgraderOS { String? _current; + UpgraderOSType? _currentOSType; String get current { if (_current != null) return _current!; @@ -29,6 +40,26 @@ class UpgraderOS { return _current ?? ''; } + UpgraderOSType get currentOSType { + if (_currentOSType != null) return _currentOSType!; + _currentOSType = isAndroid + ? UpgraderOSType.android + : isFuchsia + ? UpgraderOSType.fuchsia + : isIOS + ? UpgraderOSType.ios + : isLinux + ? UpgraderOSType.linux + : isMacOS + ? UpgraderOSType.macos + : isWeb + ? UpgraderOSType.web + : isWindows + ? UpgraderOSType.windows + : UpgraderOSType.android; + return _currentOSType ?? UpgraderOSType.android; + } + /// The target operating system. String get operatingSystem { try { @@ -109,6 +140,11 @@ class UpgraderOS { return false; } } + + @override + String toString() { + return 'operatingSystem: $operatingSystem, version: $operatingSystemVersion'; + } } /// A class to mock [UpgraderOS] for testing. diff --git a/lib/src/upgrader.dart b/lib/src/upgrader.dart index e1f5d763..4ac53d1e 100644 --- a/lib/src/upgrader.dart +++ b/lib/src/upgrader.dart @@ -26,11 +26,11 @@ typedef VoidBoolCallback = void Function(bool value); /// Signature of callback for willDisplayUpgrade. Includes display, /// minAppVersion, installedVersion, and appStoreVersion. -typedef WillDisplayUpgradeCallback = void Function( - {required bool display, - String? minAppVersion, - String? installedVersion, - String? appStoreVersion}); +typedef WillDisplayUpgradeCallback = void Function({ + required bool display, + String? installedVersion, + required UpgraderVersionInfo versionInfo, +}); /// The type of data in the stream. typedef UpgraderEvaluateNeed = bool; @@ -38,6 +38,8 @@ typedef UpgraderEvaluateNeed = bool; /// A class to define the configuration for the appcast. The configuration /// contains two parts: a URL to the appcast, and a list of supported OS /// names, such as "android", "fuchsia", "ios", "linux" "macos", "web", "windows". + +// TODO: remove this class class AppcastConfiguration { final List? supportedOS; final String? url; @@ -49,6 +51,7 @@ class AppcastConfiguration { } /// Creates a shared instance of [Upgrader]. +// TODO: maybe this should not be created as a global. Upgrader _sharedInstance = Upgrader(); /// A class to configure the upgrade dialog. @@ -66,19 +69,26 @@ class Upgrader with WidgetsBindingObserver { this.countryCode, this.languageCode, this.minAppVersion, + UpgraderStoreController? storeController, UpgraderOS? upgraderOS, }) : client = client ?? http.Client(), + storeController = storeController ?? UpgraderStoreController(), upgraderOS = upgraderOS ?? UpgraderOS() { if (debugLogging) print("upgrader: instantiated."); } /// Provide an Appcast that can be replaced for mock testing. + // TODO: remove this class final Appcast? appcast; /// The appcast configuration ([AppcastConfiguration]) used by [Appcast]. /// When an appcast is configured for iOS, the iTunes lookup is not used. + // TODO: remove this class final AppcastConfiguration? appcastConfig; + /// The controller that provides the store details for each platform. + final UpgraderStoreController storeController; + /// Provide an HTTP Client that can be replaced for mock testing. final http.Client client; @@ -118,17 +128,14 @@ class Upgrader with WidgetsBindingObserver { bool _initCalled = false; PackageInfo? _packageInfo; + PackageInfo? get packageInfo => _packageInfo; String? _installedVersion; - String? _appStoreVersion; - String? _appStoreListingURL; - String? _releaseNotes; - String? _updateAvailable; + Version? _updateAvailable; DateTime? _lastTimeAlerted; - String? _lastVersionAlerted; - String? _userIgnoredVersion; + Version? _lastVersionAlerted; + Version? _userIgnoredVersion; bool _hasAlerted = false; - bool _isCriticalUpdate = false; /// Track the initialization future so that [initialize] can be called multiple times. Future? _futureInit; @@ -148,35 +155,40 @@ class Upgrader with WidgetsBindingObserver { static const notInitializedExceptionMessage = 'upgrader: initialize() not called. Must be called first.'; - String? get currentAppStoreListingURL => _appStoreListingURL; + String? get currentAppStoreListingURL => _versionInfo?.appStoreListingURL; - String? get currentAppStoreVersion => _appStoreVersion; + String? get currentAppStoreVersion => + _versionInfo?.appStoreVersion?.toString(); String? get currentInstalledVersion => _installedVersion; - String? get releaseNotes => _releaseNotes; + String? get releaseNotes => _versionInfo?.releaseNotes; void installPackageInfo({PackageInfo? packageInfo}) { _packageInfo = packageInfo; _initCalled = false; } - void installAppStoreVersion(String version) => _appStoreVersion = version; + // void installAppStoreVersion(String version) => _appStoreVersion = version; + + // void installAppStoreListingURL(String url) => _appStoreListingURL = url; - void installAppStoreListingURL(String url) => _appStoreListingURL = url; + /// The latest version info for this app. + UpgraderVersionInfo? _versionInfo; + + /// The latest version info for this app. + UpgraderVersionInfo? get versionInfo => _versionInfo; /// Initialize [Upgrader] by getting saved preferences, getting platform package info, and getting /// released version info. Future initialize() async { - if (debugLogging) { - print('upgrader: initialize called'); - } + if (debugLogging) print('upgrader: initialize called'); + if (_futureInit != null) return _futureInit!; _futureInit = Future(() async { - if (debugLogging) { - print('upgrader: initializing'); - } + if (debugLogging) print('upgrader: initializing'); + if (_initCalled) { assert(false, 'This should never happen.'); return true; @@ -185,19 +197,7 @@ class Upgrader with WidgetsBindingObserver { await getSavedPrefs(); - if (debugLogging) { - print('upgrader: default operatingSystem: ' - '${upgraderOS.operatingSystem} ${upgraderOS.operatingSystemVersion}'); - print('upgrader: operatingSystem: ${upgraderOS.operatingSystem}'); - print('upgrader: ' - 'isAndroid: ${upgraderOS.isAndroid}, ' - 'isIOS: ${upgraderOS.isIOS}, ' - 'isLinux: ${upgraderOS.isLinux}, ' - 'isMacOS: ${upgraderOS.isMacOS}, ' - 'isWindows: ${upgraderOS.isWindows}, ' - 'isFuchsia: ${upgraderOS.isFuchsia}, ' - 'isWeb: ${upgraderOS.isWeb}'); - } + if (debugLogging) print('upgrader: $upgraderOS'); if (_packageInfo == null) { _packageInfo = await PackageInfo.fromPlatform(); @@ -211,9 +211,10 @@ class Upgrader with WidgetsBindingObserver { _installedVersion = _packageInfo!.version; - await updateVersionInfo(); + _versionInfo = await updateVersionInfo(); - // Add an observer of application events. + // Add an observer of application events, so that when the app returns + // from the background, the version info is updated. WidgetsBinding.instance.addObserver(this); _evaluationReady = true; @@ -248,116 +249,52 @@ class Upgrader with WidgetsBindingObserver { } } - Future updateVersionInfo() async { - // If there is an appcast for this platform - if (isAppcastThisPlatform()) { - if (debugLogging) { - print('upgrader: appcast is available for this platform'); - } - - final appcast = this.appcast ?? Appcast(client: client); - await appcast.parseAppcastItemsFromUri(appcastConfig!.url!); - if (debugLogging) { - var count = appcast.items == null ? 0 : appcast.items!.length; - print('upgrader: appcast item count: $count'); - } - final criticalUpdateItem = appcast.bestCriticalItem(); - final criticalVersion = criticalUpdateItem?.versionString ?? ''; - - final bestItem = appcast.bestItem(); - if (bestItem != null && - bestItem.versionString != null && - bestItem.versionString!.isNotEmpty) { - if (debugLogging) { - print( - 'upgrader: appcast best item version: ${bestItem.versionString}'); - print( - 'upgrader: appcast critical update item version: ${criticalUpdateItem?.versionString}'); - } + /// Update the version info for this app. + Future updateVersionInfo() async { + if (_packageInfo == null || _packageInfo!.packageName.isEmpty) { + return null; + } - try { - if (criticalVersion.isNotEmpty && - Version.parse(_installedVersion!) < - Version.parse(criticalVersion)) { - _isCriticalUpdate = true; - } - } catch (e) { - print('upgrader: updateVersionInfo could not parse version info $e'); - _isCriticalUpdate = false; - } + // Determine the store to be used for this app. + final store = storeController.getUpgraderStore(upgraderOS); + if (store == null) return null; - _appStoreVersion = bestItem.versionString; - _appStoreListingURL = bestItem.fileURL; - _releaseNotes = bestItem.itemDescription; - } - } else { - if (_packageInfo == null || _packageInfo!.packageName.isEmpty) { - return false; - } - - // The country code of the locale, defaulting to `US`. - final country = countryCode ?? findCountryCode(); + // Determine the installed version of this app. + late Version installedVersion; + try { + installedVersion = Version.parse(_installedVersion!); + } catch (e) { if (debugLogging) { - print('upgrader: countryCode: $country'); + print('upgrader: installedVersion exception: $e'); + return null; } + } - // The language code of the locale, defaulting to `en`. - final language = languageCode ?? findLanguageCode(); - if (debugLogging) { - print('upgrader: languageCode: $language'); - } + // Determine the country code of the locale, defaulting to `US`. + final country = countryCode ?? findCountryCode(); + if (debugLogging) { + print('upgrader: countryCode: $country'); + } - // Get Android version from Google Play Store, or - // get iOS version from iTunes Store. - if (upgraderOS.isAndroid) { - await getAndroidStoreVersion(country: country, language: language); - } else if (upgraderOS.isIOS) { - final iTunes = ITunesSearchAPI(); - iTunes.debugLogging = debugLogging; - iTunes.client = client; - final response = await (iTunes - .lookupByBundleId(_packageInfo!.packageName, country: country)); - - if (response != null) { - _appStoreVersion = iTunes.version(response); - _appStoreListingURL = iTunes.trackViewUrl(response); - _releaseNotes ??= iTunes.releaseNotes(response); - final mav = iTunes.minAppVersion(response); - if (mav != null) { - minAppVersion = mav.toString(); - if (debugLogging) { - print('upgrader: ITunesResults.minAppVersion: $minAppVersion'); - } - } - } - } + // Determine the language code of the locale, defaulting to `en`. + final language = languageCode ?? findLanguageCode(); + if (debugLogging) { + print('upgrader: languageCode: $language'); } - return true; + // Get the version info from the store. + final versionInfo = store.getVersionInfo( + upgrader: this, + installedVersion: installedVersion, + country: country, + language: language); + + return versionInfo; } /// Android info is fetched by parsing the html of the app store page. Future getAndroidStoreVersion( {String? country, String? language}) async { - final id = _packageInfo!.packageName; - final playStore = PlayStoreSearchAPI(client: client); - playStore.debugLogging = debugLogging; - final response = - await (playStore.lookupById(id, country: country, language: language)); - if (response != null) { - _appStoreVersion ??= playStore.version(response); - _appStoreListingURL ??= - playStore.lookupURLById(id, language: language, country: country); - _releaseNotes ??= playStore.releaseNotes(response); - final mav = playStore.minAppVersion(response); - if (mav != null) { - minAppVersion = mav.toString(); - if (debugLogging) { - print('upgrader: PlayStoreResults.minAppVersion: $minAppVersion'); - } - } - } - return true; } @@ -436,7 +373,7 @@ class Upgrader with WidgetsBindingObserver { } bool blocked() { - return belowMinAppVersion() || _isCriticalUpdate; + return belowMinAppVersion() || versionInfo?.isCriticalUpdate == true; } bool shouldDisplayUpgrade() { @@ -464,12 +401,12 @@ class Upgrader with WidgetsBindingObserver { } // Call the [willDisplayUpgrade] callback when available. - if (willDisplayUpgrade != null) { + if (willDisplayUpgrade != null && versionInfo != null) { willDisplayUpgrade!( - display: rv, - minAppVersion: minAppVersion, - installedVersion: _installedVersion, - appStoreVersion: _appStoreVersion); + display: rv, + installedVersion: _installedVersion, + versionInfo: versionInfo!, + ); } return rv; @@ -506,8 +443,8 @@ class Upgrader with WidgetsBindingObserver { } bool alreadyIgnoredThisVersion() { - final rv = - _userIgnoredVersion != null && _userIgnoredVersion == _appStoreVersion; + final rv = _userIgnoredVersion != null && + _userIgnoredVersion == versionInfo?.appStoreVersion; if (rv && debugLogging) { print('upgrader: alreadyIgnoredThisVersion: true'); } @@ -516,21 +453,19 @@ class Upgrader with WidgetsBindingObserver { bool isUpdateAvailable() { if (debugLogging) { - print('upgrader: appStoreVersion: $_appStoreVersion'); print('upgrader: installedVersion: $_installedVersion'); print('upgrader: minAppVersion: $minAppVersion'); } - if (_appStoreVersion == null || _installedVersion == null) { + if (versionInfo?.appStoreVersion == null || _installedVersion == null) { if (debugLogging) print('upgrader: isUpdateAvailable: false'); return false; } try { - final appStoreVersion = Version.parse(_appStoreVersion!); final installedVersion = Version.parse(_installedVersion!); - final available = appStoreVersion > installedVersion; - _updateAvailable = available ? _appStoreVersion : null; + final available = versionInfo!.appStoreVersion! > installedVersion; + _updateAvailable = available ? versionInfo?.appStoreVersion : null; } on Exception catch (e) { if (debugLogging) { print('upgrader: isUpdateAvailable: $e'); @@ -585,8 +520,9 @@ class Upgrader with WidgetsBindingObserver { Future saveIgnored() async { var prefs = await SharedPreferences.getInstance(); - _userIgnoredVersion = _appStoreVersion; - await prefs.setString('userIgnoredVersion', _userIgnoredVersion ?? ''); + _userIgnoredVersion = versionInfo?.appStoreVersion; + await prefs.setString( + 'userIgnoredVersion', _userIgnoredVersion?.toString() ?? ''); return true; } @@ -595,8 +531,9 @@ class Upgrader with WidgetsBindingObserver { _lastTimeAlerted = DateTime.now(); await prefs.setString('lastTimeAlerted', _lastTimeAlerted.toString()); - _lastVersionAlerted = _appStoreVersion; - await prefs.setString('lastVersionAlerted', _lastVersionAlerted ?? ''); + _lastVersionAlerted = versionInfo?.appStoreVersion; + await prefs.setString( + 'lastVersionAlerted', _lastVersionAlerted?.toString() ?? ''); _hasAlerted = true; return true; @@ -609,28 +546,46 @@ class Upgrader with WidgetsBindingObserver { _lastTimeAlerted = DateTime.parse(lastTimeAlerted); } - _lastVersionAlerted = prefs.getString('lastVersionAlerted'); - - _userIgnoredVersion = prefs.getString('userIgnoredVersion'); + final versionAlerted = prefs.getString('lastVersionAlerted'); + if (versionAlerted != null) { + try { + _lastVersionAlerted = Version.parse(versionAlerted); + } catch (e) { + if (debugLogging) { + print('upgrader: lastVersionAlerted exception: $e'); + } + } + } + final ignoredVersion = prefs.getString('userIgnoredVersion'); + if (ignoredVersion != null) { + try { + _userIgnoredVersion = Version.parse(ignoredVersion); + } catch (e) { + if (debugLogging) { + print('upgrader: userIgnoredVersion exception: $e'); + } + } + } return true; } void sendUserToAppStore() async { - if (_appStoreListingURL == null || _appStoreListingURL!.isEmpty) { + final appStoreListingURL = versionInfo?.appStoreListingURL; + if (appStoreListingURL == null || appStoreListingURL.isEmpty) { if (debugLogging) { - print('upgrader: empty _appStoreListingURL'); + print('upgrader: empty appStoreListingURL'); } return; } if (debugLogging) { - print('upgrader: launching: $_appStoreListingURL'); + print('upgrader: launching: $appStoreListingURL'); } - if (await canLaunchUrl(Uri.parse(_appStoreListingURL!))) { + if (await canLaunchUrl(Uri.parse(appStoreListingURL))) { try { - await launchUrl(Uri.parse(_appStoreListingURL!), + await launchUrl(Uri.parse(appStoreListingURL), mode: upgraderOS.isAndroid ? LaunchMode.externalNonBrowserApplication : LaunchMode.platformDefault); @@ -642,3 +597,286 @@ class Upgrader with WidgetsBindingObserver { } else {} } } + +class UpgraderVersionInfo { + final String? appStoreListingURL; + final Version? appStoreVersion; + final Version? installedVersion; + final bool? isCriticalUpdate; + final Version? minAppVersion; + final String? releaseNotes; + + UpgraderVersionInfo({ + this.appStoreListingURL, + this.appStoreVersion, + this.installedVersion, + this.isCriticalUpdate, + this.minAppVersion, + this.releaseNotes, + }); + + @override + String toString() { + return 'appStoreListingURL: $appStoreListingURL, ' + 'appStoreVersion: $appStoreVersion, ' + 'installedVersion: $installedVersion, ' + 'isCriticalUpdate: $isCriticalUpdate, ' + 'minAppVersion: $minAppVersion, ' + 'releaseNotes: $releaseNotes'; + } +} + +abstract class UpgraderStore { + Future getVersionInfo( + {required Upgrader upgrader, + required Version installedVersion, + required String? country, + required String? language}); +} + +class UpgraderAppStore extends UpgraderStore { + @override + Future getVersionInfo( + {required Upgrader upgrader, + required Version installedVersion, + required String? country, + required String? language}) async { + String? appStoreListingURL; + Version? appStoreVersion; + bool? isCriticalUpdate; + Version? minAppVersion; + String? releaseNotes; + + final iTunes = ITunesSearchAPI(); + iTunes.debugLogging = upgrader.debugLogging; + iTunes.client = upgrader.client; + final response = await (iTunes + .lookupByBundleId(upgrader.packageInfo!.packageName, country: country)); + + if (response != null) { + final version = iTunes.version(response); + if (version != null) { + try { + appStoreVersion = Version.parse(version); + } catch (e) { + if (upgrader.debugLogging) { + print('upgrader: UpgraderAppStore.appStoreVersion exception: $e'); + } + } + } + appStoreListingURL = iTunes.trackViewUrl(response); + releaseNotes ??= iTunes.releaseNotes(response); + minAppVersion = iTunes.minAppVersion(response); + if (minAppVersion != null) { + if (upgrader.debugLogging) { + print('upgrader: UpgraderAppStore.minAppVersion: $minAppVersion'); + } + } + } + + final versionInfo = UpgraderVersionInfo( + installedVersion: installedVersion, + appStoreListingURL: appStoreListingURL, + appStoreVersion: appStoreVersion, + isCriticalUpdate: isCriticalUpdate, + minAppVersion: minAppVersion, + releaseNotes: releaseNotes, + ); + if (upgrader.debugLogging) { + print('upgrader: UpgraderAppStore: version info: $versionInfo'); + } + return versionInfo; + } +} + +class UpgraderPlayStore extends UpgraderStore { + @override + Future getVersionInfo( + {required Upgrader upgrader, + required Version installedVersion, + required String? country, + required String? language}) async { + final id = upgrader.packageInfo!.packageName; + final playStore = PlayStoreSearchAPI(client: upgrader.client); + playStore.debugLogging = upgrader.debugLogging; + + String? appStoreListingURL; + Version? appStoreVersion; + bool? isCriticalUpdate; + Version? minAppVersion; + String? releaseNotes; + + final response = + await playStore.lookupById(id, country: country, language: language); + if (response != null) { + final version = playStore.version(response); + if (version != null) { + try { + appStoreVersion = Version.parse(version); + } catch (e) { + if (upgrader.debugLogging) { + print('upgrader: UpgraderPlayStore.appStoreVersion exception: $e'); + } + } + } + + appStoreListingURL ??= + playStore.lookupURLById(id, language: language, country: country); + releaseNotes ??= playStore.releaseNotes(response); + final mav = playStore.minAppVersion(response); + if (mav != null) { + try { + final minVersion = mav.toString(); + minAppVersion = Version.parse(minVersion); + + if (upgrader.debugLogging) { + print('upgrader: UpgraderPlayStore.minAppVersion: $minAppVersion'); + } + } catch (e) { + if (upgrader.debugLogging) { + print('upgrader: UpgraderPlayStore.minAppVersion exception: $e'); + } + } + } + } + + final versionInfo = UpgraderVersionInfo( + installedVersion: installedVersion, + appStoreListingURL: appStoreListingURL, + appStoreVersion: appStoreVersion, + isCriticalUpdate: isCriticalUpdate, + minAppVersion: minAppVersion, + releaseNotes: releaseNotes, + ); + if (upgrader.debugLogging) { + print('upgrader: UpgraderPlayStore: version info: $versionInfo'); + } + return versionInfo; + } +} + +class UpgraderAppcastStore extends UpgraderStore { + UpgraderAppcastStore({required this.appcastURL}); + + final String appcastURL; + + @override + Future getVersionInfo( + {required Upgrader upgrader, + required Version installedVersion, + required String? country, + required String? language}) async { + String? appStoreListingURL; + Version? appStoreVersion; + bool? isCriticalUpdate; + String? releaseNotes; + + final appcast = Appcast(client: upgrader.client); + await appcast.parseAppcastItemsFromUri(appcastURL); + if (upgrader.debugLogging) { + var count = appcast.items == null ? 0 : appcast.items!.length; + print('upgrader: UpgraderAppcastStore item count: $count'); + } + final criticalUpdateItem = appcast.bestCriticalItem(); + final criticalVersion = criticalUpdateItem?.versionString ?? ''; + + final bestItem = appcast.bestItem(); + if (bestItem != null && + bestItem.versionString != null && + bestItem.versionString!.isNotEmpty) { + if (upgrader.debugLogging) { + print('upgrader: UpgraderAppcastStore best item version: ' + '${bestItem.versionString}'); + print('upgrader: UpgraderAppcastStore critical update item version: ' + '${criticalUpdateItem?.versionString}'); + } + + try { + if (criticalVersion.isNotEmpty && + installedVersion < Version.parse(criticalVersion)) { + isCriticalUpdate = true; + } + } catch (e) { + if (upgrader.debugLogging) { + print( + 'upgrader: UpgraderAppcastStore: updateVersionInfo could not parse version info $e'); + } + } + + if (bestItem.versionString != null) { + try { + appStoreVersion = Version.parse(bestItem.versionString!); + } catch (e) { + if (upgrader.debugLogging) { + print( + 'upgrader: UpgraderAppcastStore: best item version could not be parsed: ' + '${bestItem.versionString}'); + } + } + } + + appStoreListingURL = bestItem.fileURL; + releaseNotes = bestItem.itemDescription; + } + + final versionInfo = UpgraderVersionInfo( + installedVersion: installedVersion, + appStoreListingURL: appStoreListingURL, + appStoreVersion: appStoreVersion, + isCriticalUpdate: isCriticalUpdate, + releaseNotes: releaseNotes, + ); + if (upgrader.debugLogging) { + print('upgrader: UpgraderAppcastStore: version info: $versionInfo'); + } + return versionInfo; + } +} + +class UpgraderConfiguration { + String get appStoreListingURL => throw UnimplementedError(); +} + +/// A controller that provides the store details for each platform. +class UpgraderStoreController { + /// Creates a controller that provides the store details for each platform. + UpgraderStoreController({ + this.onAndroid = onAndroidStore, + this.onFuchsia, + this.oniOS = onIOSStore, + this.onLinux, + this.onMacOS, + this.onWeb, + this.onWindows, + }); + + final UpgraderStore Function()? onAndroid; + final UpgraderStore Function()? onFuchsia; + final UpgraderStore Function()? oniOS; + final UpgraderStore Function()? onLinux; + final UpgraderStore Function()? onMacOS; + final UpgraderStore Function()? onWeb; + final UpgraderStore Function()? onWindows; + + UpgraderStore? getUpgraderStore(UpgraderOS upgraderOS) { + switch (upgraderOS.currentOSType) { + case UpgraderOSType.android: + return onAndroid?.call(); + case UpgraderOSType.fuchsia: + return onFuchsia?.call(); + case UpgraderOSType.ios: + return oniOS?.call(); + case UpgraderOSType.linux: + return onLinux?.call(); + case UpgraderOSType.macos: + return onMacOS?.call(); + case UpgraderOSType.web: + return onWeb?.call(); + case UpgraderOSType.windows: + return onWindows?.call(); + } + } + + static UpgraderStore onAndroidStore() => UpgraderPlayStore(); + static UpgraderStore onIOSStore() => UpgraderAppStore(); +} diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 4b455da2..d16d5300 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -115,22 +115,22 @@ void main() { expect(upgrader.currentInstalledVersion, '1.9.9'); expect(upgrader.isUpdateAvailable(), true); - upgrader.installAppStoreVersion('1.2.3'); + // upgrader.installAppStoreVersion('1.2.3'); expect(upgrader.currentAppStoreVersion, '1.2.3'); expect(upgrader.isUpdateAvailable(), false); - upgrader.installAppStoreVersion('6.2.3'); + // upgrader.installAppStoreVersion('6.2.3'); expect(upgrader.currentAppStoreVersion, '6.2.3'); expect(upgrader.isUpdateAvailable(), true); - upgrader.installAppStoreVersion('1.1.1'); + // upgrader.installAppStoreVersion('1.1.1'); expect(upgrader.currentAppStoreVersion, '1.1.1'); expect(upgrader.isUpdateAvailable(), false); await upgrader.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(upgrader.isUpdateAvailable(), true); - upgrader.installAppStoreVersion('1.1.1'); + // upgrader.installAppStoreVersion('1.1.1'); expect(upgrader.currentAppStoreVersion, '1.1.1'); expect(upgrader.isUpdateAvailable(), false); @@ -158,8 +158,8 @@ void main() { testWidgets('test installAppStoreListingURL', (WidgetTester tester) async { final upgrader = Upgrader(); - upgrader.installAppStoreListingURL( - 'https://itunes.apple.com/us/app/google-maps-transit-food/id585027354?mt=8&uo=4'); + // upgrader.installAppStoreListingURL( + // 'https://itunes.apple.com/us/app/google-maps-transit-food/id585027354?mt=8&uo=4'); expect(upgrader.currentAppStoreListingURL, 'https://itunes.apple.com/us/app/google-maps-transit-food/id585027354?mt=8&uo=4'); @@ -727,16 +727,16 @@ void main() { await upgrader.initialize(); var notCalled = true; - upgrader.willDisplayUpgrade = ( - {required bool display, - String? minAppVersion, - String? installedVersion, - String? appStoreVersion}) { + upgrader.willDisplayUpgrade = ({ + required bool display, + String? installedVersion, + required UpgraderVersionInfo versionInfo, + }) { expect(display, true); expect(installedVersion, '1.9.6'); /// Appcast Test critical version. - expect(appStoreVersion, '3.0.0'); + expect(versionInfo.appStoreVersion, '3.0.0'); notCalled = false; }; @@ -775,14 +775,14 @@ void main() { await upgrader.initialize(); var notCalled = true; - upgrader.willDisplayUpgrade = ( - {required bool display, - String? minAppVersion, - String? installedVersion, - String? appStoreVersion}) { + upgrader.willDisplayUpgrade = ({ + required bool display, + String? installedVersion, + required UpgraderVersionInfo versionInfo, + }) { expect(display, true); expect(installedVersion, '1.9.6'); - expect(appStoreVersion, '2.3.2'); + expect(versionInfo.appStoreVersion, '2.3.2'); notCalled = false; }; @@ -850,15 +850,15 @@ void main() { // Test the willDisplayUpgrade callback var notCalled = true; - upgrader.willDisplayUpgrade = ( - {required bool display, - String? minAppVersion, - String? installedVersion, - String? appStoreVersion}) { + upgrader.willDisplayUpgrade = ({ + required bool display, + String? installedVersion, + required UpgraderVersionInfo versionInfo, + }) { expect(display, false); - expect(minAppVersion, isNull); + expect(versionInfo.minAppVersion, isNull); expect(installedVersion, isNull); - expect(appStoreVersion, isNull); + expect(versionInfo.appStoreVersion, isNull); notCalled = false; }; expect(upgrader.shouldDisplayUpgrade(), false); @@ -866,15 +866,15 @@ void main() { upgrader.debugDisplayAlways = true; notCalled = true; - upgrader.willDisplayUpgrade = ( - {required bool display, - String? minAppVersion, - String? installedVersion, - String? appStoreVersion}) { + upgrader.willDisplayUpgrade = ({ + required bool display, + String? installedVersion, + required UpgraderVersionInfo versionInfo, + }) { expect(display, true); - expect(minAppVersion, isNull); + expect(versionInfo.minAppVersion, isNull); expect(installedVersion, isNull); - expect(appStoreVersion, isNull); + expect(versionInfo.appStoreVersion, isNull); notCalled = false; }; expect(upgrader.shouldDisplayUpgrade(), true); @@ -898,16 +898,16 @@ void main() { await upgrader.initialize(); var notCalled = true; - upgrader.willDisplayUpgrade = ( - {required bool display, - String? minAppVersion, - String? installedVersion, - String? appStoreVersion}) { + upgrader.willDisplayUpgrade = ({ + required bool display, + String? installedVersion, + required UpgraderVersionInfo versionInfo, + }) { expect(display, true); - expect(minAppVersion, '2.0.0'); + expect(versionInfo.minAppVersion, '2.0.0'); expect(upgrader.minAppVersion, '2.0.0'); expect(installedVersion, '1.9.6'); - expect(appStoreVersion, '5.6'); + expect(versionInfo.appStoreVersion, '5.6'); notCalled = false; }; From ae75645f268e484ad221eb34009d174f6f56d965 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sat, 20 Jan 2024 16:37:31 -0500 Subject: [PATCH 02/15] Implemented [UpgraderState] that is used internally to replace evaluation ready. Removed Appcast configuration so that an Appcast [UpgraderStore] can be used. --- CHANGELOG.md | 5 + README.md | 7 +- example/lib/main.dart | 12 +- example/lib/main_subclass.dart | 2 +- lib/src/upgrade_alert.dart | 52 +-- lib/src/upgrade_card.dart | 38 +- lib/src/upgrade_state.dart | 44 +++ lib/src/upgrade_store_controller.dart | 270 +++++++++++++++ lib/src/upgrader.dart | 478 ++++---------------------- lib/src/upgrader_version_info.dart | 31 ++ lib/upgrader.dart | 5 +- test/fake_appcast.dart | 21 +- test/upgrader_test.dart | 42 +-- 13 files changed, 513 insertions(+), 494 deletions(-) create mode 100644 lib/src/upgrade_state.dart create mode 100644 lib/src/upgrade_store_controller.dart create mode 100644 lib/src/upgrader_version_info.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 16c9d575..c846911d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## Next + +- Implemented [UpgraderState] that is used internally to replace evaluation ready. +- Removed Appcast configration so that an Appcast [UpgraderStore] can be used. + ## 9.0.0-alpha.3 - [371] Added the parameter `dialogKey` to `UpgraderAlert` that is used by the alert dialog. diff --git a/README.md b/README.md index 6d1371ec..fc8d7035 100644 --- a/README.md +++ b/README.md @@ -433,8 +433,11 @@ UpgradeAlert(Upgrader(messages: UpgraderMessages(code: 'es'))); ## Semantic Versioning The `upgrader` package uses the [version](https://pub.dev/packages/version) package that -is in compliance with the Semantic Versioning spec at http://semver.org/. - +is in compliance with the Semantic Versioning spec at http://semver.org/. It converts any +version string to a 3 digit version: MAJOR.MINOR.PATCH. For versions that only use 1 +digit (MAJOR), it converts it to a 3 digit version: MAJOR.0.0, and for versions that +only use 2 digits (MAJOR.MINOR), it converts it to a 3 digit version: MAJOR.MINOR.0, to +be compliant with Semantic Versioning. ## iTunes Search API diff --git a/example/lib/main.dart b/example/lib/main.dart index 253e667c..828d9954 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023 Larry Aasen. All rights reserved. + * Copyright (c) 2019-2024 Larry Aasen. All rights reserved. */ import 'package:flutter/material.dart'; @@ -27,11 +27,11 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Upgrader Example', home: UpgradeAlert( - upgrader: Upgrader(debugLogging: true), - child: Scaffold( - appBar: AppBar(title: const Text('Upgrader Example')), - body: const Center(child: Text('Checking...')), - )), + child: Scaffold( + appBar: AppBar(title: const Text('Upgrader Example')), + body: const Center(child: Text('Checking...')), + ), + ), ); } } diff --git a/example/lib/main_subclass.dart b/example/lib/main_subclass.dart index cffa3457..42a9d9f8 100644 --- a/example/lib/main_subclass.dart +++ b/example/lib/main_subclass.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 Larry Aasen. All rights reserved. + * Copyright (c) 2019-2024 Larry Aasen. All rights reserved. */ import 'package:flutter/material.dart'; diff --git a/lib/src/upgrade_alert.dart b/lib/src/upgrade_alert.dart index ea73e611..3ed857db 100644 --- a/lib/src/upgrade_alert.dart +++ b/lib/src/upgrade_alert.dart @@ -1,11 +1,12 @@ /* - * Copyright (c) 2021-2023 Larry Aasen. All rights reserved. + * Copyright (c) 2021-2024 Larry Aasen. All rights reserved. */ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'upgrade_messages.dart'; +import 'upgrade_state.dart'; import 'upgrader.dart'; /// There are two different dialog styles: Cupertino and Material @@ -99,29 +100,30 @@ class UpgradeAlertState extends State { /// Describes the part of the user interface represented by this widget. @override Widget build(BuildContext context) { - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: build UpgradeAlert'); } return StreamBuilder( - initialData: widget.upgrader.evaluationReady, - stream: widget.upgrader.evaluationStream, - builder: - (BuildContext context, AsyncSnapshot snapshot) { + initialData: widget.upgrader.state, + stream: widget.upgrader.stateStream, + builder: (BuildContext context, AsyncSnapshot snapshot) { if ((snapshot.connectionState == ConnectionState.waiting || snapshot.connectionState == ConnectionState.active) && - snapshot.data != null && - snapshot.data!) { - if (widget.upgrader.debugLogging) { - print("upgrader: need to evaluate version"); - } - - if (!displayed) { - final checkContext = widget.navigatorKey != null && - widget.navigatorKey!.currentContext != null - ? widget.navigatorKey!.currentContext! - : context; - checkVersion(context: checkContext); + snapshot.data != null) { + final upgraderState = snapshot.data!; + if (upgraderState.versionInfo != null) { + if (widget.upgrader.state.debugLogging) { + print("upgrader: need to evaluate version"); + } + + if (!displayed) { + final checkContext = widget.navigatorKey != null && + widget.navigatorKey!.currentContext != null + ? widget.navigatorKey!.currentContext! + : context; + checkVersion(context: checkContext); + } } } return widget.child ?? const SizedBox.shrink(); @@ -133,7 +135,7 @@ class UpgradeAlertState extends State { /// Only called by [UpgradeAlert] and not used by [UpgradeCard]. void checkVersion({required BuildContext context}) { final shouldDisplay = widget.upgrader.shouldDisplayUpgrade(); - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: shouldDisplayReleaseNotes: $shouldDisplayReleaseNotes'); } if (shouldDisplay) { @@ -156,7 +158,7 @@ class UpgradeAlertState extends State { } void onUserIgnored(BuildContext context, bool shouldPop) { - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: button tapped: ignore'); } @@ -173,7 +175,7 @@ class UpgradeAlertState extends State { } void onUserLater(BuildContext context, bool shouldPop) { - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: button tapped: later'); } @@ -186,7 +188,7 @@ class UpgradeAlertState extends State { } void onUserUpdated(BuildContext context, bool shouldPop) { - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: button tapped: update now'); } @@ -221,7 +223,7 @@ class UpgradeAlertState extends State { required bool canDismissDialog, required UpgraderMessages messages, }) { - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: showTheDialog title: $title'); print('upgrader: showTheDialog message: $message'); print('upgrader: showTheDialog releaseNotes: $releaseNotes'); @@ -253,12 +255,12 @@ class UpgradeAlertState extends State { /// is false. Also called when the back button is pressed. Return true for /// the screen to be popped. Defaults to false. bool onWillPop() { - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: onWillPop called'); } if (widget.shouldPopScope != null) { final should = widget.shouldPopScope!(); - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: shouldPopScope=$should'); } return should; diff --git a/lib/src/upgrade_card.dart b/lib/src/upgrade_card.dart index 369a5b51..3f8a17e9 100644 --- a/lib/src/upgrade_card.dart +++ b/lib/src/upgrade_card.dart @@ -1,11 +1,12 @@ /* - * Copyright (c) 2021-2023 Larry Aasen. All rights reserved. + * Copyright (c) 2021-2024 Larry Aasen. All rights reserved. */ import 'package:flutter/material.dart'; import 'alert_style_widget.dart'; import 'upgrade_messages.dart'; +import 'upgrade_state.dart'; import 'upgrader.dart'; /// A widget to display the upgrade card. @@ -78,25 +79,26 @@ class UpgradeCardState extends State { /// Describes the part of the user interface represented by this widget. @override Widget build(BuildContext context) { - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: build UpgradeCard'); } return StreamBuilder( - initialData: widget.upgrader.evaluationReady, - stream: widget.upgrader.evaluationStream, - builder: (BuildContext context, - AsyncSnapshot snapshot) { + initialData: widget.upgrader.state, + stream: widget.upgrader.stateStream, + builder: (BuildContext context, AsyncSnapshot snapshot) { if ((snapshot.connectionState == ConnectionState.waiting || snapshot.connectionState == ConnectionState.active) && - snapshot.data != null && - snapshot.data!) { - if (widget.upgrader.shouldDisplayUpgrade()) { - return buildUpgradeCard( - context, const Key('upgrader_alert_card')); - } else { - if (widget.upgrader.debugLogging) { - print('upgrader: UpgradeCard will not display'); + snapshot.data != null) { + final upgraderState = snapshot.data!; + if (upgraderState.versionInfo != null) { + if (widget.upgrader.shouldDisplayUpgrade()) { + return buildUpgradeCard( + context, const Key('upgrader_alert_card')); + } else { + if (widget.upgrader.state.debugLogging) { + print('upgrader: UpgradeCard will not display'); + } } } } @@ -111,7 +113,7 @@ class UpgradeCardState extends State { final message = widget.upgrader.body(appMessages); final releaseNotes = widget.upgrader.releaseNotes; - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: UpgradeCard: will display'); print('upgrader: UpgradeCard: showDialog title: $title'); print('upgrader: UpgradeCard: showDialog message: $message'); @@ -208,7 +210,7 @@ class UpgradeCardState extends State { (widget.upgrader.releaseNotes?.isNotEmpty ?? false); void onUserIgnored() { - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: button tapped: ignore'); } @@ -223,7 +225,7 @@ class UpgradeCardState extends State { } void onUserLater() { - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: button tapped: later'); } @@ -234,7 +236,7 @@ class UpgradeCardState extends State { } void onUserUpdated() { - if (widget.upgrader.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: button tapped: update now'); } diff --git a/lib/src/upgrade_state.dart b/lib/src/upgrade_state.dart new file mode 100644 index 00000000..6e53d350 --- /dev/null +++ b/lib/src/upgrade_state.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2024 Larry Aasen. All rights reserved. + +import 'package:http/http.dart' as http; +import 'package:package_info_plus/package_info_plus.dart'; + +import 'upgrader_version_info.dart'; + +/// The [Upgrader] state. +class UpgraderState { + /// Creates an [Upgrader] state. + UpgraderState({ + required this.client, + required this.debugLogging, + this.packageInfo, + this.versionInfo, + }); + + /// Provide an HTTP Client that can be replaced for mock testing. + final http.Client client; + + /// Enable print statements for debugging. + final bool debugLogging; + + /// The app package metadata information. + final PackageInfo? packageInfo; + + /// The latest version info for this app. + final UpgraderVersionInfo? versionInfo; + + /// Creates a new state object by copying existing data and modifying selected fields. + UpgraderState copyWith({ + http.Client? client, + bool? debugLogging, + PackageInfo? packageInfo, + UpgraderVersionInfo? versionInfo, + }) { + return UpgraderState( + client: client ?? this.client, + debugLogging: debugLogging ?? this.debugLogging, + packageInfo: packageInfo ?? this.packageInfo, + versionInfo: versionInfo ?? this.versionInfo, + ); + } +} diff --git a/lib/src/upgrade_store_controller.dart b/lib/src/upgrade_store_controller.dart new file mode 100644 index 00000000..3f1c1689 --- /dev/null +++ b/lib/src/upgrade_store_controller.dart @@ -0,0 +1,270 @@ +// Copyright (c) 2024 Larry Aasen. All rights reserved. + +import 'dart:async'; + +import 'package:version/version.dart'; + +import 'appcast.dart'; +import 'itunes_search_api.dart'; +import 'play_store_search_api.dart'; +import 'upgrade_os.dart'; +import 'upgrade_state.dart'; +import 'upgrader_version_info.dart'; + +abstract class UpgraderStore { + Future getVersionInfo( + {required UpgraderState state, + required Version installedVersion, + required String? country, + required String? language}); +} + +class UpgraderAppStore extends UpgraderStore { + @override + Future getVersionInfo( + {required UpgraderState state, + required Version installedVersion, + required String? country, + required String? language}) async { + if (state.packageInfo == null) return UpgraderVersionInfo(); + + String? appStoreListingURL; + Version? appStoreVersion; + bool? isCriticalUpdate; + Version? minAppVersion; + String? releaseNotes; + + final iTunes = ITunesSearchAPI(); + iTunes.debugLogging = state.debugLogging; + iTunes.client = state.client; + final response = await (iTunes + .lookupByBundleId(state.packageInfo!.packageName, country: country)); + + if (response != null) { + final version = iTunes.version(response); + if (version != null) { + try { + appStoreVersion = Version.parse(version); + } catch (e) { + if (state.debugLogging) { + print('upgrader: UpgraderAppStore.appStoreVersion exception: $e'); + } + } + } + appStoreListingURL = iTunes.trackViewUrl(response); + releaseNotes ??= iTunes.releaseNotes(response); + minAppVersion = iTunes.minAppVersion(response); + if (minAppVersion != null) { + if (state.debugLogging) { + print('upgrader: UpgraderAppStore.minAppVersion: $minAppVersion'); + } + } + } + + final versionInfo = UpgraderVersionInfo( + installedVersion: installedVersion, + appStoreListingURL: appStoreListingURL, + appStoreVersion: appStoreVersion, + isCriticalUpdate: isCriticalUpdate, + minAppVersion: minAppVersion, + releaseNotes: releaseNotes, + ); + if (state.debugLogging) { + print('upgrader: UpgraderAppStore: version info: $versionInfo'); + } + return versionInfo; + } +} + +class UpgraderPlayStore extends UpgraderStore { + @override + Future getVersionInfo( + {required UpgraderState state, + required Version installedVersion, + required String? country, + required String? language}) async { + if (state.packageInfo == null) return UpgraderVersionInfo(); + final id = state.packageInfo!.packageName; + final playStore = PlayStoreSearchAPI(client: state.client); + playStore.debugLogging = state.debugLogging; + + String? appStoreListingURL; + Version? appStoreVersion; + bool? isCriticalUpdate; + Version? minAppVersion; + String? releaseNotes; + + final response = + await playStore.lookupById(id, country: country, language: language); + if (response != null) { + final version = playStore.version(response); + if (version != null) { + try { + appStoreVersion = Version.parse(version); + } catch (e) { + if (state.debugLogging) { + print('upgrader: UpgraderPlayStore.appStoreVersion exception: $e'); + } + } + } + + appStoreListingURL ??= + playStore.lookupURLById(id, language: language, country: country); + releaseNotes ??= playStore.releaseNotes(response); + final mav = playStore.minAppVersion(response); + if (mav != null) { + try { + final minVersion = mav.toString(); + minAppVersion = Version.parse(minVersion); + + if (state.debugLogging) { + print('upgrader: UpgraderPlayStore.minAppVersion: $minAppVersion'); + } + } catch (e) { + if (state.debugLogging) { + print('upgrader: UpgraderPlayStore.minAppVersion exception: $e'); + } + } + } + } + + final versionInfo = UpgraderVersionInfo( + installedVersion: installedVersion, + appStoreListingURL: appStoreListingURL, + appStoreVersion: appStoreVersion, + isCriticalUpdate: isCriticalUpdate, + minAppVersion: minAppVersion, + releaseNotes: releaseNotes, + ); + if (state.debugLogging) { + print('upgrader: UpgraderPlayStore: version info: $versionInfo'); + } + return versionInfo; + } +} + +class UpgraderAppcastStore extends UpgraderStore { + UpgraderAppcastStore({required this.appcastURL}); + + final String appcastURL; + + @override + Future getVersionInfo( + {required UpgraderState state, + required Version installedVersion, + required String? country, + required String? language}) async { + String? appStoreListingURL; + Version? appStoreVersion; + bool? isCriticalUpdate; + String? releaseNotes; + + final appcast = Appcast(client: state.client); + await appcast.parseAppcastItemsFromUri(appcastURL); + if (state.debugLogging) { + var count = appcast.items == null ? 0 : appcast.items!.length; + print('upgrader: UpgraderAppcastStore item count: $count'); + } + final criticalUpdateItem = appcast.bestCriticalItem(); + final criticalVersion = criticalUpdateItem?.versionString ?? ''; + + final bestItem = appcast.bestItem(); + if (bestItem != null && + bestItem.versionString != null && + bestItem.versionString!.isNotEmpty) { + if (state.debugLogging) { + print('upgrader: UpgraderAppcastStore best item version: ' + '${bestItem.versionString}'); + print('upgrader: UpgraderAppcastStore critical update item version: ' + '${criticalUpdateItem?.versionString}'); + } + + try { + if (criticalVersion.isNotEmpty && + installedVersion < Version.parse(criticalVersion)) { + isCriticalUpdate = true; + } + } catch (e) { + if (state.debugLogging) { + print( + 'upgrader: UpgraderAppcastStore: updateVersionInfo could not parse version info $e'); + } + } + + if (bestItem.versionString != null) { + try { + appStoreVersion = Version.parse(bestItem.versionString!); + } catch (e) { + if (state.debugLogging) { + print( + 'upgrader: UpgraderAppcastStore: best item version could not be parsed: ' + '${bestItem.versionString}'); + } + } + } + + appStoreListingURL = bestItem.fileURL; + releaseNotes = bestItem.itemDescription; + } + + final versionInfo = UpgraderVersionInfo( + installedVersion: installedVersion, + appStoreListingURL: appStoreListingURL, + appStoreVersion: appStoreVersion, + isCriticalUpdate: isCriticalUpdate, + releaseNotes: releaseNotes, + ); + if (state.debugLogging) { + print('upgrader: UpgraderAppcastStore: version info: $versionInfo'); + } + return versionInfo; + } +} + +class UpgraderConfiguration { + String get appStoreListingURL => throw UnimplementedError(); +} + +/// A controller that provides the store details for each platform. +class UpgraderStoreController { + /// Creates a controller that provides the store details for each platform. + UpgraderStoreController({ + this.onAndroid = onAndroidStore, + this.onFuchsia, + this.oniOS = onIOSStore, + this.onLinux, + this.onMacOS, + this.onWeb, + this.onWindows, + }); + + final UpgraderStore Function()? onAndroid; + final UpgraderStore Function()? onFuchsia; + final UpgraderStore Function()? oniOS; + final UpgraderStore Function()? onLinux; + final UpgraderStore Function()? onMacOS; + final UpgraderStore Function()? onWeb; + final UpgraderStore Function()? onWindows; + + UpgraderStore? getUpgraderStore(UpgraderOS upgraderOS) { + switch (upgraderOS.currentOSType) { + case UpgraderOSType.android: + return onAndroid?.call(); + case UpgraderOSType.fuchsia: + return onFuchsia?.call(); + case UpgraderOSType.ios: + return oniOS?.call(); + case UpgraderOSType.linux: + return onLinux?.call(); + case UpgraderOSType.macos: + return onMacOS?.call(); + case UpgraderOSType.web: + return onWeb?.call(); + case UpgraderOSType.windows: + return onWindows?.call(); + } + } + + static UpgraderStore onAndroidStore() => UpgraderPlayStore(); + static UpgraderStore onIOSStore() => UpgraderAppStore(); +} diff --git a/lib/src/upgrader.dart b/lib/src/upgrader.dart index 4ac53d1e..300c3e53 100644 --- a/lib/src/upgrader.dart +++ b/lib/src/upgrader.dart @@ -1,6 +1,4 @@ -/* - * Copyright (c) 2018-2023 Larry Aasen. All rights reserved. - */ +// Copyright (c) 2018-2024 Larry Aasen. All rights reserved. import 'dart:async'; import 'dart:ui'; @@ -12,11 +10,11 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:version/version.dart'; -import 'appcast.dart'; -import 'itunes_search_api.dart'; -import 'play_store_search_api.dart'; -import 'upgrade_os.dart'; import 'upgrade_messages.dart'; +import 'upgrade_os.dart'; +import 'upgrade_state.dart'; +import 'upgrade_store_controller.dart'; +import 'upgrader_version_info.dart'; /// Signature of callbacks that have no arguments and return bool. typedef BoolCallback = bool Function(); @@ -32,37 +30,16 @@ typedef WillDisplayUpgradeCallback = void Function({ required UpgraderVersionInfo versionInfo, }); -/// The type of data in the stream. -typedef UpgraderEvaluateNeed = bool; - -/// A class to define the configuration for the appcast. The configuration -/// contains two parts: a URL to the appcast, and a list of supported OS -/// names, such as "android", "fuchsia", "ios", "linux" "macos", "web", "windows". - -// TODO: remove this class -class AppcastConfiguration { - final List? supportedOS; - final String? url; - - AppcastConfiguration({ - this.supportedOS, - this.url, - }); -} - /// Creates a shared instance of [Upgrader]. -// TODO: maybe this should not be created as a global. Upgrader _sharedInstance = Upgrader(); /// A class to configure the upgrade dialog. class Upgrader with WidgetsBindingObserver { Upgrader({ - this.appcastConfig, - this.appcast, this.messages, this.debugDisplayAlways = false, this.debugDisplayOnce = false, - this.debugLogging = false, + bool debugLogging = false, this.durationUntilAlertAgain = const Duration(days: 3), this.willDisplayUpgrade, http.Client? client, @@ -71,27 +48,20 @@ class Upgrader with WidgetsBindingObserver { this.minAppVersion, UpgraderStoreController? storeController, UpgraderOS? upgraderOS, - }) : client = client ?? http.Client(), + }) : _state = UpgraderState( + client: client ?? http.Client(), debugLogging: debugLogging), storeController = storeController ?? UpgraderStoreController(), upgraderOS = upgraderOS ?? UpgraderOS() { - if (debugLogging) print("upgrader: instantiated."); + if (debugLogging) print("upgrader: instantiated"); } - /// Provide an Appcast that can be replaced for mock testing. - // TODO: remove this class - final Appcast? appcast; - - /// The appcast configuration ([AppcastConfiguration]) used by [Appcast]. - /// When an appcast is configured for iOS, the iTunes lookup is not used. - // TODO: remove this class - final AppcastConfiguration? appcastConfig; + /// The [Upgrader] state. + UpgraderState _state; + UpgraderState get state => _state; /// The controller that provides the store details for each platform. final UpgraderStoreController storeController; - /// Provide an HTTP Client that can be replaced for mock testing. - final http.Client client; - /// The country code that will override the system locale. Optional. final String? countryCode; @@ -104,9 +74,6 @@ class Upgrader with WidgetsBindingObserver { /// For debugging, display the upgrade at least once once. bool debugDisplayOnce; - /// Enable print statements for debugging. - bool debugLogging; - /// Duration until alerting user again final Duration durationUntilAlertAgain; @@ -127,9 +94,6 @@ class Upgrader with WidgetsBindingObserver { WillDisplayUpgradeCallback? willDisplayUpgrade; bool _initCalled = false; - PackageInfo? _packageInfo; - PackageInfo? get packageInfo => _packageInfo; - String? _installedVersion; Version? _updateAvailable; DateTime? _lastTimeAlerted; @@ -140,14 +104,10 @@ class Upgrader with WidgetsBindingObserver { /// Track the initialization future so that [initialize] can be called multiple times. Future? _futureInit; - /// A stream that provides a new value each time an evaluation should be performed. - /// The values will always be null or true. - Stream get evaluationStream => _streamController.stream; - final _streamController = StreamController.broadcast(); - - /// An evaluation should be performed. - bool get evaluationReady => _evaluationReady; - bool _evaluationReady = false; + /// A stream that provides a new state each time an evaluation should be performed. + /// The values will always be the state. + Stream get stateStream => _streamController.stream; + final _streamController = StreamController.broadcast(); /// A shared instance of [Upgrader]. static Upgrader get sharedInstance => _sharedInstance; @@ -155,17 +115,18 @@ class Upgrader with WidgetsBindingObserver { static const notInitializedExceptionMessage = 'upgrader: initialize() not called. Must be called first.'; - String? get currentAppStoreListingURL => _versionInfo?.appStoreListingURL; + String? get currentAppStoreListingURL => + state.versionInfo?.appStoreListingURL; String? get currentAppStoreVersion => - _versionInfo?.appStoreVersion?.toString(); + state.versionInfo?.appStoreVersion?.toString(); String? get currentInstalledVersion => _installedVersion; - String? get releaseNotes => _versionInfo?.releaseNotes; + String? get releaseNotes => state.versionInfo?.releaseNotes; void installPackageInfo({PackageInfo? packageInfo}) { - _packageInfo = packageInfo; + updateState(state.copyWith(packageInfo: packageInfo)); _initCalled = false; } @@ -174,20 +135,17 @@ class Upgrader with WidgetsBindingObserver { // void installAppStoreListingURL(String url) => _appStoreListingURL = url; /// The latest version info for this app. - UpgraderVersionInfo? _versionInfo; - - /// The latest version info for this app. - UpgraderVersionInfo? get versionInfo => _versionInfo; + UpgraderVersionInfo? get versionInfo => state.versionInfo; /// Initialize [Upgrader] by getting saved preferences, getting platform package info, and getting /// released version info. Future initialize() async { - if (debugLogging) print('upgrader: initialize called'); + if (state.debugLogging) print('upgrader: initialize called'); if (_futureInit != null) return _futureInit!; _futureInit = Future(() async { - if (debugLogging) print('upgrader: initializing'); + if (state.debugLogging) print('upgrader: initializing'); if (_initCalled) { assert(false, 'This should never happen.'); @@ -197,37 +155,43 @@ class Upgrader with WidgetsBindingObserver { await getSavedPrefs(); - if (debugLogging) print('upgrader: $upgraderOS'); + if (state.debugLogging) print('upgrader: $upgraderOS'); - if (_packageInfo == null) { - _packageInfo = await PackageInfo.fromPlatform(); - if (debugLogging) { + if (state.packageInfo == null) { + updateState( + state.copyWith(packageInfo: await PackageInfo.fromPlatform())); + if (state.debugLogging) { + print( + 'upgrader: package info packageName: ${state.packageInfo!.packageName}'); print( - 'upgrader: package info packageName: ${_packageInfo!.packageName}'); - print('upgrader: package info appName: ${_packageInfo!.appName}'); - print('upgrader: package info version: ${_packageInfo!.version}'); + 'upgrader: package info appName: ${state.packageInfo!.appName}'); + print( + 'upgrader: package info version: ${state.packageInfo!.version}'); } } - _installedVersion = _packageInfo!.version; + _installedVersion = state.packageInfo!.version; - _versionInfo = await updateVersionInfo(); + updateState(state.copyWith(versionInfo: await updateVersionInfo())); // Add an observer of application events, so that when the app returns // from the background, the version info is updated. WidgetsBinding.instance.addObserver(this); - _evaluationReady = true; - /// Trigger the stream to indicate an evaluation should be performed. - /// The value will always be true. - _streamController.add(true); + /// The value will always be the state. + _streamController.add(state); return true; }); return _futureInit!; } + /// Update the Upgrader state. + void updateState(UpgraderState newState) { + _state = newState; + } + /// Remove any resources allocated. void dispose() { // Remove the observer of application events. @@ -236,22 +200,23 @@ class Upgrader with WidgetsBindingObserver { /// Handle application events. @override - Future didChangeAppLifecycleState(AppLifecycleState state) async { - super.didChangeAppLifecycleState(state); + Future didChangeAppLifecycleState( + AppLifecycleState lifecycleState) async { + super.didChangeAppLifecycleState(lifecycleState); // When app has resumed from background. - if (state == AppLifecycleState.resumed) { + if (lifecycleState == AppLifecycleState.resumed) { await updateVersionInfo(); /// Trigger the stream to indicate another evaluation should be performed. - /// The value will always be true. - _streamController.add(true); + /// The value will always be the state. + _streamController.add(state); } } /// Update the version info for this app. Future updateVersionInfo() async { - if (_packageInfo == null || _packageInfo!.packageName.isEmpty) { + if (state.packageInfo == null || state.packageInfo!.packageName.isEmpty) { return null; } @@ -264,7 +229,7 @@ class Upgrader with WidgetsBindingObserver { try { installedVersion = Version.parse(_installedVersion!); } catch (e) { - if (debugLogging) { + if (state.debugLogging) { print('upgrader: installedVersion exception: $e'); return null; } @@ -272,19 +237,19 @@ class Upgrader with WidgetsBindingObserver { // Determine the country code of the locale, defaulting to `US`. final country = countryCode ?? findCountryCode(); - if (debugLogging) { + if (state.debugLogging) { print('upgrader: countryCode: $country'); } // Determine the language code of the locale, defaulting to `en`. final language = languageCode ?? findLanguageCode(); - if (debugLogging) { + if (state.debugLogging) { print('upgrader: languageCode: $language'); } // Get the version info from the store. final versionInfo = store.getVersionInfo( - upgrader: this, + state: state, installedVersion: installedVersion, country: country, language: language); @@ -298,24 +263,6 @@ class Upgrader with WidgetsBindingObserver { return true; } - bool isAppcastThisPlatform() { - if (appcastConfig == null || - appcastConfig!.url == null || - appcastConfig!.url!.isEmpty) { - return false; - } - - // Since this appcast config contains a URL, this appcast is valid. - // However, if the supported OS is not listed, it is not supported. - // When there are no supported OSes listed, they are all supported. - var supported = true; - if (appcastConfig!.supportedOS != null) { - supported = - appcastConfig!.supportedOS!.contains(upgraderOS.operatingSystem); - } - return supported; - } - bool verifyInit() { if (!_initCalled) { throw ('upgrader: initialize() not called. Must be called first.'); @@ -325,7 +272,7 @@ class Upgrader with WidgetsBindingObserver { String appName() { verifyInit(); - return _packageInfo?.appName ?? ''; + return state.packageInfo?.appName ?? ''; } String body(UpgraderMessages messages) { @@ -352,7 +299,7 @@ class Upgrader with WidgetsBindingObserver { final locale = Localizations.localeOf(context); // Get the current language code in the app. languageCode = locale.languageCode; - if (debugLogging) { + if (state.debugLogging) { print('upgrader: current locale: $locale'); } } catch (e) { @@ -364,7 +311,7 @@ class Upgrader with WidgetsBindingObserver { if (appMessages.languageCode.isEmpty) { print('upgrader: error -> languageCode is empty'); - } else if (debugLogging) { + } else if (state.debugLogging) { print('upgrader: languageCode: ${appMessages.languageCode}'); } @@ -379,7 +326,7 @@ class Upgrader with WidgetsBindingObserver { bool shouldDisplayUpgrade() { final isBlocked = blocked(); - if (debugLogging) { + if (state.debugLogging) { print('upgrader: blocked: $isBlocked'); print('upgrader: debugDisplayAlways: $debugDisplayAlways'); print('upgrader: debugDisplayOnce: $debugDisplayOnce'); @@ -396,7 +343,7 @@ class Upgrader with WidgetsBindingObserver { } else if (isTooSoon() || alreadyIgnoredThisVersion()) { rv = false; } - if (debugLogging) { + if (state.debugLogging) { print('upgrader: shouldDisplayUpgrade: $rv'); } @@ -421,7 +368,7 @@ class Upgrader with WidgetsBindingObserver { final installedVersion = Version.parse(_installedVersion!); rv = installedVersion < minVersion; } catch (e) { - if (debugLogging) { + if (state.debugLogging) { print(e); } } @@ -436,7 +383,7 @@ class Upgrader with WidgetsBindingObserver { final lastAlertedDuration = DateTime.now().difference(_lastTimeAlerted!); final rv = lastAlertedDuration < durationUntilAlertAgain; - if (rv && debugLogging) { + if (rv && state.debugLogging) { print('upgrader: isTooSoon: true'); } return rv; @@ -445,19 +392,19 @@ class Upgrader with WidgetsBindingObserver { bool alreadyIgnoredThisVersion() { final rv = _userIgnoredVersion != null && _userIgnoredVersion == versionInfo?.appStoreVersion; - if (rv && debugLogging) { + if (rv && state.debugLogging) { print('upgrader: alreadyIgnoredThisVersion: true'); } return rv; } bool isUpdateAvailable() { - if (debugLogging) { + if (state.debugLogging) { print('upgrader: installedVersion: $_installedVersion'); print('upgrader: minAppVersion: $minAppVersion'); } if (versionInfo?.appStoreVersion == null || _installedVersion == null) { - if (debugLogging) print('upgrader: isUpdateAvailable: false'); + if (state.debugLogging) print('upgrader: isUpdateAvailable: false'); return false; } @@ -467,12 +414,12 @@ class Upgrader with WidgetsBindingObserver { final available = versionInfo!.appStoreVersion! > installedVersion; _updateAvailable = available ? versionInfo?.appStoreVersion : null; } on Exception catch (e) { - if (debugLogging) { + if (state.debugLogging) { print('upgrader: isUpdateAvailable: $e'); } } final isAvailable = _updateAvailable != null; - if (debugLogging) print('upgrader: isUpdateAvailable: $isAvailable'); + if (state.debugLogging) print('upgrader: isUpdateAvailable: $isAvailable'); return isAvailable; } @@ -551,7 +498,7 @@ class Upgrader with WidgetsBindingObserver { try { _lastVersionAlerted = Version.parse(versionAlerted); } catch (e) { - if (debugLogging) { + if (state.debugLogging) { print('upgrader: lastVersionAlerted exception: $e'); } } @@ -561,7 +508,7 @@ class Upgrader with WidgetsBindingObserver { try { _userIgnoredVersion = Version.parse(ignoredVersion); } catch (e) { - if (debugLogging) { + if (state.debugLogging) { print('upgrader: userIgnoredVersion exception: $e'); } } @@ -573,13 +520,13 @@ class Upgrader with WidgetsBindingObserver { void sendUserToAppStore() async { final appStoreListingURL = versionInfo?.appStoreListingURL; if (appStoreListingURL == null || appStoreListingURL.isEmpty) { - if (debugLogging) { + if (state.debugLogging) { print('upgrader: empty appStoreListingURL'); } return; } - if (debugLogging) { + if (state.debugLogging) { print('upgrader: launching: $appStoreListingURL'); } @@ -590,293 +537,10 @@ class Upgrader with WidgetsBindingObserver { ? LaunchMode.externalNonBrowserApplication : LaunchMode.platformDefault); } catch (e) { - if (debugLogging) { + if (state.debugLogging) { print('upgrader: launch to app store failed: $e'); } } } else {} } } - -class UpgraderVersionInfo { - final String? appStoreListingURL; - final Version? appStoreVersion; - final Version? installedVersion; - final bool? isCriticalUpdate; - final Version? minAppVersion; - final String? releaseNotes; - - UpgraderVersionInfo({ - this.appStoreListingURL, - this.appStoreVersion, - this.installedVersion, - this.isCriticalUpdate, - this.minAppVersion, - this.releaseNotes, - }); - - @override - String toString() { - return 'appStoreListingURL: $appStoreListingURL, ' - 'appStoreVersion: $appStoreVersion, ' - 'installedVersion: $installedVersion, ' - 'isCriticalUpdate: $isCriticalUpdate, ' - 'minAppVersion: $minAppVersion, ' - 'releaseNotes: $releaseNotes'; - } -} - -abstract class UpgraderStore { - Future getVersionInfo( - {required Upgrader upgrader, - required Version installedVersion, - required String? country, - required String? language}); -} - -class UpgraderAppStore extends UpgraderStore { - @override - Future getVersionInfo( - {required Upgrader upgrader, - required Version installedVersion, - required String? country, - required String? language}) async { - String? appStoreListingURL; - Version? appStoreVersion; - bool? isCriticalUpdate; - Version? minAppVersion; - String? releaseNotes; - - final iTunes = ITunesSearchAPI(); - iTunes.debugLogging = upgrader.debugLogging; - iTunes.client = upgrader.client; - final response = await (iTunes - .lookupByBundleId(upgrader.packageInfo!.packageName, country: country)); - - if (response != null) { - final version = iTunes.version(response); - if (version != null) { - try { - appStoreVersion = Version.parse(version); - } catch (e) { - if (upgrader.debugLogging) { - print('upgrader: UpgraderAppStore.appStoreVersion exception: $e'); - } - } - } - appStoreListingURL = iTunes.trackViewUrl(response); - releaseNotes ??= iTunes.releaseNotes(response); - minAppVersion = iTunes.minAppVersion(response); - if (minAppVersion != null) { - if (upgrader.debugLogging) { - print('upgrader: UpgraderAppStore.minAppVersion: $minAppVersion'); - } - } - } - - final versionInfo = UpgraderVersionInfo( - installedVersion: installedVersion, - appStoreListingURL: appStoreListingURL, - appStoreVersion: appStoreVersion, - isCriticalUpdate: isCriticalUpdate, - minAppVersion: minAppVersion, - releaseNotes: releaseNotes, - ); - if (upgrader.debugLogging) { - print('upgrader: UpgraderAppStore: version info: $versionInfo'); - } - return versionInfo; - } -} - -class UpgraderPlayStore extends UpgraderStore { - @override - Future getVersionInfo( - {required Upgrader upgrader, - required Version installedVersion, - required String? country, - required String? language}) async { - final id = upgrader.packageInfo!.packageName; - final playStore = PlayStoreSearchAPI(client: upgrader.client); - playStore.debugLogging = upgrader.debugLogging; - - String? appStoreListingURL; - Version? appStoreVersion; - bool? isCriticalUpdate; - Version? minAppVersion; - String? releaseNotes; - - final response = - await playStore.lookupById(id, country: country, language: language); - if (response != null) { - final version = playStore.version(response); - if (version != null) { - try { - appStoreVersion = Version.parse(version); - } catch (e) { - if (upgrader.debugLogging) { - print('upgrader: UpgraderPlayStore.appStoreVersion exception: $e'); - } - } - } - - appStoreListingURL ??= - playStore.lookupURLById(id, language: language, country: country); - releaseNotes ??= playStore.releaseNotes(response); - final mav = playStore.minAppVersion(response); - if (mav != null) { - try { - final minVersion = mav.toString(); - minAppVersion = Version.parse(minVersion); - - if (upgrader.debugLogging) { - print('upgrader: UpgraderPlayStore.minAppVersion: $minAppVersion'); - } - } catch (e) { - if (upgrader.debugLogging) { - print('upgrader: UpgraderPlayStore.minAppVersion exception: $e'); - } - } - } - } - - final versionInfo = UpgraderVersionInfo( - installedVersion: installedVersion, - appStoreListingURL: appStoreListingURL, - appStoreVersion: appStoreVersion, - isCriticalUpdate: isCriticalUpdate, - minAppVersion: minAppVersion, - releaseNotes: releaseNotes, - ); - if (upgrader.debugLogging) { - print('upgrader: UpgraderPlayStore: version info: $versionInfo'); - } - return versionInfo; - } -} - -class UpgraderAppcastStore extends UpgraderStore { - UpgraderAppcastStore({required this.appcastURL}); - - final String appcastURL; - - @override - Future getVersionInfo( - {required Upgrader upgrader, - required Version installedVersion, - required String? country, - required String? language}) async { - String? appStoreListingURL; - Version? appStoreVersion; - bool? isCriticalUpdate; - String? releaseNotes; - - final appcast = Appcast(client: upgrader.client); - await appcast.parseAppcastItemsFromUri(appcastURL); - if (upgrader.debugLogging) { - var count = appcast.items == null ? 0 : appcast.items!.length; - print('upgrader: UpgraderAppcastStore item count: $count'); - } - final criticalUpdateItem = appcast.bestCriticalItem(); - final criticalVersion = criticalUpdateItem?.versionString ?? ''; - - final bestItem = appcast.bestItem(); - if (bestItem != null && - bestItem.versionString != null && - bestItem.versionString!.isNotEmpty) { - if (upgrader.debugLogging) { - print('upgrader: UpgraderAppcastStore best item version: ' - '${bestItem.versionString}'); - print('upgrader: UpgraderAppcastStore critical update item version: ' - '${criticalUpdateItem?.versionString}'); - } - - try { - if (criticalVersion.isNotEmpty && - installedVersion < Version.parse(criticalVersion)) { - isCriticalUpdate = true; - } - } catch (e) { - if (upgrader.debugLogging) { - print( - 'upgrader: UpgraderAppcastStore: updateVersionInfo could not parse version info $e'); - } - } - - if (bestItem.versionString != null) { - try { - appStoreVersion = Version.parse(bestItem.versionString!); - } catch (e) { - if (upgrader.debugLogging) { - print( - 'upgrader: UpgraderAppcastStore: best item version could not be parsed: ' - '${bestItem.versionString}'); - } - } - } - - appStoreListingURL = bestItem.fileURL; - releaseNotes = bestItem.itemDescription; - } - - final versionInfo = UpgraderVersionInfo( - installedVersion: installedVersion, - appStoreListingURL: appStoreListingURL, - appStoreVersion: appStoreVersion, - isCriticalUpdate: isCriticalUpdate, - releaseNotes: releaseNotes, - ); - if (upgrader.debugLogging) { - print('upgrader: UpgraderAppcastStore: version info: $versionInfo'); - } - return versionInfo; - } -} - -class UpgraderConfiguration { - String get appStoreListingURL => throw UnimplementedError(); -} - -/// A controller that provides the store details for each platform. -class UpgraderStoreController { - /// Creates a controller that provides the store details for each platform. - UpgraderStoreController({ - this.onAndroid = onAndroidStore, - this.onFuchsia, - this.oniOS = onIOSStore, - this.onLinux, - this.onMacOS, - this.onWeb, - this.onWindows, - }); - - final UpgraderStore Function()? onAndroid; - final UpgraderStore Function()? onFuchsia; - final UpgraderStore Function()? oniOS; - final UpgraderStore Function()? onLinux; - final UpgraderStore Function()? onMacOS; - final UpgraderStore Function()? onWeb; - final UpgraderStore Function()? onWindows; - - UpgraderStore? getUpgraderStore(UpgraderOS upgraderOS) { - switch (upgraderOS.currentOSType) { - case UpgraderOSType.android: - return onAndroid?.call(); - case UpgraderOSType.fuchsia: - return onFuchsia?.call(); - case UpgraderOSType.ios: - return oniOS?.call(); - case UpgraderOSType.linux: - return onLinux?.call(); - case UpgraderOSType.macos: - return onMacOS?.call(); - case UpgraderOSType.web: - return onWeb?.call(); - case UpgraderOSType.windows: - return onWindows?.call(); - } - } - - static UpgraderStore onAndroidStore() => UpgraderPlayStore(); - static UpgraderStore onIOSStore() => UpgraderAppStore(); -} diff --git a/lib/src/upgrader_version_info.dart b/lib/src/upgrader_version_info.dart new file mode 100644 index 00000000..125074ea --- /dev/null +++ b/lib/src/upgrader_version_info.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2024 Larry Aasen. All rights reserved. + +import 'package:version/version.dart'; + +class UpgraderVersionInfo { + final String? appStoreListingURL; + final Version? appStoreVersion; + final Version? installedVersion; + final bool? isCriticalUpdate; + final Version? minAppVersion; + final String? releaseNotes; + + UpgraderVersionInfo({ + this.appStoreListingURL, + this.appStoreVersion, + this.installedVersion, + this.isCriticalUpdate, + this.minAppVersion, + this.releaseNotes, + }); + + @override + String toString() { + return 'appStoreListingURL: $appStoreListingURL, ' + 'appStoreVersion: $appStoreVersion, ' + 'installedVersion: $installedVersion, ' + 'isCriticalUpdate: $isCriticalUpdate, ' + 'minAppVersion: $minAppVersion, ' + 'releaseNotes: $releaseNotes'; + } +} diff --git a/lib/upgrader.dart b/lib/upgrader.dart index 811cea11..130e93ae 100644 --- a/lib/upgrader.dart +++ b/lib/upgrader.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2023 Larry Aasen. All rights reserved. + * Copyright (c) 2018-2024 Larry Aasen. All rights reserved. */ library upgrader; @@ -13,4 +13,7 @@ export 'src/upgrade_card.dart'; export 'src/upgrade_device.dart'; export 'src/upgrade_messages.dart'; export 'src/upgrade_os.dart'; +export 'src/upgrade_state.dart'; +export 'src/upgrade_store_controller.dart'; export 'src/upgrader.dart'; +export 'src/upgrader_version_info.dart'; diff --git a/test/fake_appcast.dart b/test/fake_appcast.dart index 493422a7..8f5af7da 100644 --- a/test/fake_appcast.dart +++ b/test/fake_appcast.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:mockito/mockito.dart'; import 'package:upgrader/src/appcast.dart'; -import 'package:upgrader/src/upgrader.dart'; import 'appcast_test.dart'; @@ -55,16 +54,16 @@ class FakeAppcast extends Fake implements TestAppcast { return [AppcastItem()]; } - AppcastConfiguration config = - AppcastConfiguration(url: 'http://some.fakewebsite.com', supportedOS: [ - 'android', - 'fuchsia', - 'ios', - 'linux', - 'macos', - 'web', - 'windows', - ]); + // AppcastConfiguration config = + // AppcastConfiguration(url: 'http://some.fakewebsite.com', supportedOS: [ + // 'android', + // 'fuchsia', + // 'ios', + // 'linux', + // 'macos', + // 'web', + // 'windows', + // ]); @override List? items = []; diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index d16d5300..6f554fde 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2023 Larry Aasen. All rights reserved. + * Copyright (c) 2018-2024 Larry Aasen. All rights reserved. */ import 'package:flutter/cupertino.dart'; @@ -111,7 +111,7 @@ void main() { expect(await upgrader.initialize(), isTrue); expect(upgrader.appName(), 'Upgrader'); - expect(upgrader.currentAppStoreVersion, '5.6'); + expect(upgrader.currentAppStoreVersion, '5.6.0'); expect(upgrader.currentInstalledVersion, '1.9.9'); expect(upgrader.isUpdateAvailable(), true); @@ -680,12 +680,14 @@ void main() { final fakeAppcast = FakeAppcast(); final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader( - upgraderOS: MockUpgraderOS(os: 'ios', ios: true), - client: client, - debugLogging: true, - appcastConfig: fakeAppcast.config, - appcast: fakeAppcast) - ..installPackageInfo( + upgraderOS: MockUpgraderOS(os: 'ios', ios: true), + client: client, + debugLogging: true, + storeController: UpgraderStoreController( + oniOS: () => UpgraderAppcastStore( + appcastURL: 'https://sparkle-project.org/test/testappcast.xml'), + ), + )..installPackageInfo( packageInfo: PackageInfo( appName: 'Upgrader', packageName: 'com.larryaasen.upgrader', @@ -703,18 +705,15 @@ void main() { final upgraderOS = MockUpgraderOS(android: true); final Client mockClient = setupMockClient(filePath: 'test/testappcast_critical.xml'); - final appcast = Appcast( - client: mockClient, - upgraderOS: upgraderOS, - upgraderDevice: MockUpgraderDevice()); final upgrader = Upgrader( + client: mockClient, upgraderOS: upgraderOS, debugLogging: true, - appcastConfig: AppcastConfiguration( - url: 'https://sparkle-project.org/test/testappcast.xml', + storeController: UpgraderStoreController( + onAndroid: () => UpgraderAppcastStore( + appcastURL: 'https://sparkle-project.org/test/testappcast.xml'), ), - appcast: appcast, )..installPackageInfo( packageInfo: PackageInfo( appName: 'Upgrader', @@ -751,18 +750,15 @@ void main() { final Client mockClient = setupMockClient(filePath: 'test/testappcastmulti.xml'); - final appcast = Appcast( - client: mockClient, - upgraderOS: upgraderOS, - upgraderDevice: MockUpgraderDevice()); final upgrader = Upgrader( + client: mockClient, upgraderOS: upgraderOS, debugLogging: true, - appcastConfig: AppcastConfiguration( - url: 'https://sparkle-project.org/test/testappcast.xml', + storeController: UpgraderStoreController( + oniOS: () => UpgraderAppcastStore( + appcastURL: 'https://sparkle-project.org/test/testappcast.xml'), ), - appcast: appcast, )..installPackageInfo( packageInfo: PackageInfo( appName: 'Upgrader', @@ -907,7 +903,7 @@ void main() { expect(versionInfo.minAppVersion, '2.0.0'); expect(upgrader.minAppVersion, '2.0.0'); expect(installedVersion, '1.9.6'); - expect(versionInfo.appStoreVersion, '5.6'); + expect(versionInfo.appStoreVersion, '5.6.0'); notCalled = false; }; From b2ec7511b15500927e796bec61391acd224813bc Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sat, 20 Jan 2024 16:38:27 -0500 Subject: [PATCH 03/15] Updated CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c846911d..9b46240f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## Next - Implemented [UpgraderState] that is used internally to replace evaluation ready. -- Removed Appcast configration so that an Appcast [UpgraderStore] can be used. +- Removed Appcast configuration so that an Appcast [UpgraderStore] can be used. ## 9.0.0-alpha.3 From e3aa04688f9e7865ef73f0f3673d65e1b61d086a Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Tue, 30 Jan 2024 08:12:02 -0500 Subject: [PATCH 04/15] Fixed all unit tests. Improved UpgraderState and UpgraderStores. --- lib/src/upgrade_state.dart | 77 ++++++- lib/src/upgrade_store_controller.dart | 33 ++- lib/src/upgrader.dart | 123 +++++----- test/fake_appcast.dart | 5 + test/upgrader_test.dart | 319 +++++++++++++------------- 5 files changed, 336 insertions(+), 221 deletions(-) diff --git a/lib/src/upgrade_state.dart b/lib/src/upgrade_state.dart index 6e53d350..2ae5af69 100644 --- a/lib/src/upgrade_state.dart +++ b/lib/src/upgrade_state.dart @@ -2,7 +2,10 @@ import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:version/version.dart'; +import 'upgrade_device.dart'; +import 'upgrade_os.dart'; import 'upgrader_version_info.dart'; /// The [Upgrader] state. @@ -10,35 +13,105 @@ class UpgraderState { /// Creates an [Upgrader] state. UpgraderState({ required this.client, - required this.debugLogging, + this.countryCodeOverride, + this.debugDisplayAlways = false, + this.debugDisplayOnce = false, + this.debugLogging = false, + this.languageCodeOverride, + this.minAppVersion, this.packageInfo, + required this.upgraderDevice, + required this.upgraderOS, this.versionInfo, }); - /// Provide an HTTP Client that can be replaced for mock testing. + /// Provide an HTTP Client that can be replaced during testing. final http.Client client; + /// The country code that will override the system locale. Optional. + final String? countryCodeOverride; + + /// For debugging, always force the upgrade to be available. + final bool debugDisplayAlways; + + /// For debugging, display the upgrade at least once once. + final bool debugDisplayOnce; + /// Enable print statements for debugging. final bool debugLogging; + /// The country code that will override the system locale. Optional. Used only for Android. + final String? languageCodeOverride; + + /// The minimum app version supported by this app. Earlier versions of this app + /// will be forced to update to the current version. Optional. + final Version? minAppVersion; + /// The app package metadata information. final PackageInfo? packageInfo; + /// Provide [UpgraderDevice] that ca be replaced during testing. + final UpgraderDevice upgraderDevice; + + /// Provides information on which OS this code is running on, and can be + /// replaced during testing. + final UpgraderOS upgraderOS; + /// The latest version info for this app. final UpgraderVersionInfo? versionInfo; /// Creates a new state object by copying existing data and modifying selected fields. UpgraderState copyWith({ http.Client? client, + String? countryCodeOverride, + bool? debugDisplayAlways, + bool? debugDisplayOnce, bool? debugLogging, + String? languageCodeOverride, + Version? minAppVersion, PackageInfo? packageInfo, + UpgraderDevice? upgraderDevice, + UpgraderOS? upgraderOS, UpgraderVersionInfo? versionInfo, }) { return UpgraderState( client: client ?? this.client, + countryCodeOverride: countryCodeOverride ?? this.countryCodeOverride, + debugDisplayAlways: debugDisplayAlways ?? this.debugDisplayAlways, + debugDisplayOnce: debugDisplayOnce ?? this.debugDisplayOnce, debugLogging: debugLogging ?? this.debugLogging, + languageCodeOverride: languageCodeOverride ?? this.languageCodeOverride, + minAppVersion: minAppVersion ?? this.minAppVersion, packageInfo: packageInfo ?? this.packageInfo, + upgraderDevice: upgraderDevice ?? this.upgraderDevice, + upgraderOS: upgraderOS ?? this.upgraderOS, versionInfo: versionInfo ?? this.versionInfo, ); } + + /// Creates a new state object by copying existing data and modifying selected fields, + /// but true parameters will null out values in the state object. + UpgraderState copyWithNull({ + bool? countryCodeOverride, + bool? languageCodeOverride, + bool? minAppVersion, + bool? packageInfo, + bool? versionInfo, + }) { + return UpgraderState( + client: client, + countryCodeOverride: + countryCodeOverride == true ? null : this.countryCodeOverride, + debugDisplayAlways: debugDisplayAlways, + debugDisplayOnce: debugDisplayOnce, + debugLogging: debugLogging, + languageCodeOverride: + languageCodeOverride == true ? null : this.languageCodeOverride, + minAppVersion: minAppVersion == true ? null : this.minAppVersion, + packageInfo: packageInfo == true ? null : this.packageInfo, + upgraderDevice: upgraderDevice, + upgraderOS: upgraderOS, + versionInfo: versionInfo == true ? null : this.versionInfo, + ); + } } diff --git a/lib/src/upgrade_store_controller.dart b/lib/src/upgrade_store_controller.dart index 3f1c1689..d6caa1e4 100644 --- a/lib/src/upgrade_store_controller.dart +++ b/lib/src/upgrade_store_controller.dart @@ -144,9 +144,24 @@ class UpgraderPlayStore extends UpgraderStore { } class UpgraderAppcastStore extends UpgraderStore { - UpgraderAppcastStore({required this.appcastURL}); + UpgraderAppcastStore({ + required this.appcastURL, + this.appcast, + // this.client, + }); final String appcastURL; + final Appcast? appcast; + // final http.Client? client; + + // /// Provide an HTTP Client that can be replaced during testing. + // final http.Client client; + + // /// Provide [UpgraderOS] that can be replaced during testing. + // final UpgraderOS upgraderOS; + + // /// Provide [UpgraderDevice] that ca be replaced during testing. + // final UpgraderDevice upgraderDevice; @override Future getVersionInfo( @@ -159,16 +174,20 @@ class UpgraderAppcastStore extends UpgraderStore { bool? isCriticalUpdate; String? releaseNotes; - final appcast = Appcast(client: state.client); - await appcast.parseAppcastItemsFromUri(appcastURL); + final localAppcast = appcast ?? + Appcast( + client: state.client, + upgraderDevice: state.upgraderDevice, + upgraderOS: state.upgraderOS); + await localAppcast.parseAppcastItemsFromUri(appcastURL); if (state.debugLogging) { - var count = appcast.items == null ? 0 : appcast.items!.length; + var count = localAppcast.items == null ? 0 : localAppcast.items!.length; print('upgrader: UpgraderAppcastStore item count: $count'); } - final criticalUpdateItem = appcast.bestCriticalItem(); + final criticalUpdateItem = localAppcast.bestCriticalItem(); final criticalVersion = criticalUpdateItem?.versionString ?? ''; - final bestItem = appcast.bestItem(); + final bestItem = localAppcast.bestItem(); if (bestItem != null && bestItem.versionString != null && bestItem.versionString!.isNotEmpty) { @@ -187,7 +206,7 @@ class UpgraderAppcastStore extends UpgraderStore { } catch (e) { if (state.debugLogging) { print( - 'upgrader: UpgraderAppcastStore: updateVersionInfo could not parse version info $e'); + 'upgrader: UpgraderAppcastStore: getVersionInfo could not parse version info $e'); } } diff --git a/lib/src/upgrader.dart b/lib/src/upgrader.dart index 300c3e53..0e150738 100644 --- a/lib/src/upgrader.dart +++ b/lib/src/upgrader.dart @@ -10,6 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:version/version.dart'; +import 'upgrade_device.dart'; import 'upgrade_messages.dart'; import 'upgrade_os.dart'; import 'upgrade_state.dart'; @@ -23,11 +24,11 @@ typedef BoolCallback = bool Function(); typedef VoidBoolCallback = void Function(bool value); /// Signature of callback for willDisplayUpgrade. Includes display, -/// minAppVersion, installedVersion, and appStoreVersion. +/// installedVersion, and versionInfo. typedef WillDisplayUpgradeCallback = void Function({ required bool display, String? installedVersion, - required UpgraderVersionInfo versionInfo, + UpgraderVersionInfo? versionInfo, }); /// Creates a shared instance of [Upgrader]. @@ -37,21 +38,31 @@ Upgrader _sharedInstance = Upgrader(); class Upgrader with WidgetsBindingObserver { Upgrader({ this.messages, - this.debugDisplayAlways = false, - this.debugDisplayOnce = false, + bool debugDisplayAlways = false, + bool debugDisplayOnce = false, bool debugLogging = false, this.durationUntilAlertAgain = const Duration(days: 3), this.willDisplayUpgrade, http.Client? client, - this.countryCode, - this.languageCode, - this.minAppVersion, + String? countryCode, + String? languageCode, + String? minAppVersion, UpgraderStoreController? storeController, + UpgraderDevice? upgraderDevice, UpgraderOS? upgraderOS, }) : _state = UpgraderState( - client: client ?? http.Client(), debugLogging: debugLogging), - storeController = storeController ?? UpgraderStoreController(), - upgraderOS = upgraderOS ?? UpgraderOS() { + client: client ?? http.Client(), + countryCodeOverride: countryCode, + debugDisplayAlways: debugDisplayAlways, + debugDisplayOnce: debugDisplayOnce, + debugLogging: debugLogging, + languageCodeOverride: languageCode, + minAppVersion: + _parseVersion(minAppVersion, 'minAppVersion', debugLogging), + upgraderDevice: upgraderDevice ?? UpgraderDevice(), + upgraderOS: upgraderOS ?? UpgraderOS(), + ), + storeController = storeController ?? UpgraderStoreController() { if (debugLogging) print("upgrader: instantiated"); } @@ -62,31 +73,12 @@ class Upgrader with WidgetsBindingObserver { /// The controller that provides the store details for each platform. final UpgraderStoreController storeController; - /// The country code that will override the system locale. Optional. - final String? countryCode; - - /// The country code that will override the system locale. Optional. Used only for Android. - final String? languageCode; - - /// For debugging, always force the upgrade to be available. - bool debugDisplayAlways; - - /// For debugging, display the upgrade at least once once. - bool debugDisplayOnce; - /// Duration until alerting user again final Duration durationUntilAlertAgain; /// The localized messages used for display in upgrader. UpgraderMessages? messages; - /// The minimum app version supported by this app. Earlier versions of this app - /// will be forced to update to the current version. Optional. - String? minAppVersion; - - /// Provides information on which OS this code is running on. - final UpgraderOS upgraderOS; - /// Called when [Upgrader] determines that an upgrade may or may not be /// displayed. The [value] parameter will be true when it should be displayed, /// and false when it should not be displayed. One good use for this callback @@ -130,6 +122,34 @@ class Upgrader with WidgetsBindingObserver { _initCalled = false; } + /// The minAppVersion in the Upgrader state. + String? get minAppVersion => state.minAppVersion.toString(); + + set minAppVersion(String? version) { + if (version == null) { + updateState(state.copyWithNull(minAppVersion: true)); + } else { + final parsedVersion = + _parseVersion(version, 'minAppVersion', state.debugLogging); + if (parsedVersion != null) { + updateState(state.copyWith(minAppVersion: parsedVersion)); + } + } + } + + static Version? _parseVersion( + String? version, String name, bool debugLogging) { + if (version == null) return null; + try { + return Version.parse(version); + } catch (e) { + // if (state.debugLogging) { + print('upgrader: _parseVersion $name exception: $e'); + // } + return null; + } + } + // void installAppStoreVersion(String version) => _appStoreVersion = version; // void installAppStoreListingURL(String url) => _appStoreListingURL = url; @@ -155,7 +175,7 @@ class Upgrader with WidgetsBindingObserver { await getSavedPrefs(); - if (state.debugLogging) print('upgrader: $upgraderOS'); + if (state.debugLogging) print('upgrader: ${state.upgraderOS}'); if (state.packageInfo == null) { updateState( @@ -178,18 +198,21 @@ class Upgrader with WidgetsBindingObserver { // from the background, the version info is updated. WidgetsBinding.instance.addObserver(this); - /// Trigger the stream to indicate an evaluation should be performed. - /// The value will always be the state. - _streamController.add(state); - return true; }); return _futureInit!; } - /// Update the Upgrader state. + /// Updates the Upgrader state. void updateState(UpgraderState newState) { _state = newState; + updateStream(); + } + + /// Updates the stream with the current state, which triggers the stream to + /// indicate an evaluation should be performed. + void updateStream() { + _streamController.add(_state); } /// Remove any resources allocated. @@ -206,11 +229,7 @@ class Upgrader with WidgetsBindingObserver { // When app has resumed from background. if (lifecycleState == AppLifecycleState.resumed) { - await updateVersionInfo(); - - /// Trigger the stream to indicate another evaluation should be performed. - /// The value will always be the state. - _streamController.add(state); + updateState(state.copyWith(versionInfo: await updateVersionInfo())); } } @@ -221,7 +240,7 @@ class Upgrader with WidgetsBindingObserver { } // Determine the store to be used for this app. - final store = storeController.getUpgraderStore(upgraderOS); + final store = storeController.getUpgraderStore(state.upgraderOS); if (store == null) return null; // Determine the installed version of this app. @@ -236,13 +255,13 @@ class Upgrader with WidgetsBindingObserver { } // Determine the country code of the locale, defaulting to `US`. - final country = countryCode ?? findCountryCode(); + final country = state.countryCodeOverride ?? findCountryCode(); if (state.debugLogging) { print('upgrader: countryCode: $country'); } // Determine the language code of the locale, defaulting to `en`. - final language = languageCode ?? findLanguageCode(); + final language = state.languageCodeOverride ?? findLanguageCode(); if (state.debugLogging) { print('upgrader: languageCode: $language'); } @@ -328,13 +347,13 @@ class Upgrader with WidgetsBindingObserver { if (state.debugLogging) { print('upgrader: blocked: $isBlocked'); - print('upgrader: debugDisplayAlways: $debugDisplayAlways'); - print('upgrader: debugDisplayOnce: $debugDisplayOnce'); + print('upgrader: debugDisplayAlways: ${state.debugDisplayAlways}'); + print('upgrader: debugDisplayOnce: ${state.debugDisplayOnce}'); print('upgrader: hasAlerted: $_hasAlerted'); } bool rv = true; - if (debugDisplayAlways || (debugDisplayOnce && !_hasAlerted)) { + if (state.debugDisplayAlways || (state.debugDisplayOnce && !_hasAlerted)) { rv = true; } else if (!isUpdateAvailable()) { rv = false; @@ -348,11 +367,11 @@ class Upgrader with WidgetsBindingObserver { } // Call the [willDisplayUpgrade] callback when available. - if (willDisplayUpgrade != null && versionInfo != null) { + if (willDisplayUpgrade != null) { willDisplayUpgrade!( display: rv, installedVersion: _installedVersion, - versionInfo: versionInfo!, + versionInfo: versionInfo, ); } @@ -362,9 +381,9 @@ class Upgrader with WidgetsBindingObserver { /// Is installed version below minimum app version? bool belowMinAppVersion() { var rv = false; - if (minAppVersion != null) { + final minVersion = state.minAppVersion ?? versionInfo?.minAppVersion; + if (minVersion != null) { try { - final minVersion = Version.parse(minAppVersion!); final installedVersion = Version.parse(_installedVersion!); rv = installedVersion < minVersion; } catch (e) { @@ -401,7 +420,7 @@ class Upgrader with WidgetsBindingObserver { bool isUpdateAvailable() { if (state.debugLogging) { print('upgrader: installedVersion: $_installedVersion'); - print('upgrader: minAppVersion: $minAppVersion'); + print('upgrader: minAppVersion: ${state.minAppVersion}'); } if (versionInfo?.appStoreVersion == null || _installedVersion == null) { if (state.debugLogging) print('upgrader: isUpdateAvailable: false'); @@ -533,7 +552,7 @@ class Upgrader with WidgetsBindingObserver { if (await canLaunchUrl(Uri.parse(appStoreListingURL))) { try { await launchUrl(Uri.parse(appStoreListingURL), - mode: upgraderOS.isAndroid + mode: state.upgraderOS.isAndroid ? LaunchMode.externalNonBrowserApplication : LaunchMode.platformDefault); } catch (e) { diff --git a/test/fake_appcast.dart b/test/fake_appcast.dart index 8f5af7da..cf329547 100644 --- a/test/fake_appcast.dart +++ b/test/fake_appcast.dart @@ -5,10 +5,12 @@ import 'dart:io'; import 'package:mockito/mockito.dart'; import 'package:upgrader/src/appcast.dart'; +import 'package:upgrader/src/upgrade_device.dart'; import 'appcast_test.dart'; class FakeAppcast extends Fake implements TestAppcast { + FakeAppcast(); int callCount = 0; @override @@ -54,6 +56,9 @@ class FakeAppcast extends Fake implements TestAppcast { return [AppcastItem()]; } + @override + UpgraderDevice get upgraderDevice => MockUpgraderDevice(); + // AppcastConfiguration config = // AppcastConfiguration(url: 'http://some.fakewebsite.com', supportedOS: [ // 'android', diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 6f554fde..67e72c88 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -10,6 +10,7 @@ import 'package:http/src/client.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:upgrader/upgrader.dart'; +import 'package:version/version.dart'; import 'appcast_test.dart'; import 'fake_appcast.dart'; @@ -115,22 +116,26 @@ void main() { expect(upgrader.currentInstalledVersion, '1.9.9'); expect(upgrader.isUpdateAvailable(), true); - // upgrader.installAppStoreVersion('1.2.3'); + upgrader.updateState(upgrader.state.copyWith( + versionInfo: UpgraderVersionInfo(appStoreVersion: Version(1, 2, 3)))); expect(upgrader.currentAppStoreVersion, '1.2.3'); expect(upgrader.isUpdateAvailable(), false); - // upgrader.installAppStoreVersion('6.2.3'); + upgrader.updateState(upgrader.state.copyWith( + versionInfo: UpgraderVersionInfo(appStoreVersion: Version(6, 2, 3)))); expect(upgrader.currentAppStoreVersion, '6.2.3'); expect(upgrader.isUpdateAvailable(), true); - // upgrader.installAppStoreVersion('1.1.1'); + upgrader.updateState(upgrader.state.copyWith( + versionInfo: UpgraderVersionInfo(appStoreVersion: Version(1, 1, 1)))); expect(upgrader.currentAppStoreVersion, '1.1.1'); expect(upgrader.isUpdateAvailable(), false); await upgrader.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(upgrader.isUpdateAvailable(), true); - // upgrader.installAppStoreVersion('1.1.1'); + upgrader.updateState(upgrader.state.copyWith( + versionInfo: UpgraderVersionInfo(appStoreVersion: Version(1, 1, 1)))); expect(upgrader.currentAppStoreVersion, '1.1.1'); expect(upgrader.isUpdateAvailable(), false); @@ -156,15 +161,6 @@ void main() { }); }); - testWidgets('test installAppStoreListingURL', (WidgetTester tester) async { - final upgrader = Upgrader(); - // upgrader.installAppStoreListingURL( - // 'https://itunes.apple.com/us/app/google-maps-transit-food/id585027354?mt=8&uo=4'); - - expect(upgrader.currentAppStoreListingURL, - 'https://itunes.apple.com/us/app/google-maps-transit-food/id585027354?mt=8&uo=4'); - }, skip: false); - testWidgets('test UpgradeAlert', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader( @@ -595,7 +591,7 @@ void main() { await tester.pumpAndSettle(); expect(upgrader.belowMinAppVersion(), true); - expect(upgrader.minAppVersion, '4.5.6'); + expect(upgrader.state.versionInfo?.minAppVersion.toString(), '4.5.6'); }, skip: false); testWidgets('test upgrader minAppVersion description ios', @@ -618,7 +614,7 @@ void main() { await tester.pumpAndSettle(); expect(upgrader.belowMinAppVersion(), true); - expect(upgrader.minAppVersion, '4.5.6'); + expect(upgrader.state.versionInfo?.minAppVersion.toString(), '4.5.6'); }, skip: false); testWidgets('test UpgradeWidget unknown app', (WidgetTester tester) async { @@ -675,173 +671,176 @@ void main() { expect(notCalled, true); }, skip: false); - group('initialize', () { - test('should use fake Appcast', () async { - final fakeAppcast = FakeAppcast(); - final client = MockITunesSearchClient.setupMockClient(); - final upgrader = Upgrader( - upgraderOS: MockUpgraderOS(os: 'ios', ios: true), - client: client, - debugLogging: true, - storeController: UpgraderStoreController( - oniOS: () => UpgraderAppcastStore( - appcastURL: 'https://sparkle-project.org/test/testappcast.xml'), + test('should use fake Appcast', () async { + final fakeAppcast = FakeAppcast(); + final client = MockITunesSearchClient.setupMockClient(); + final upgrader = Upgrader( + upgraderOS: MockUpgraderOS(os: 'ios', ios: true), + client: client, + debugLogging: true, + storeController: UpgraderStoreController( + oniOS: () => UpgraderAppcastStore( + appcastURL: 'https://sparkle-project.org/test/testappcast.xml', + appcast: fakeAppcast, ), - )..installPackageInfo( - packageInfo: PackageInfo( - appName: 'Upgrader', - packageName: 'com.larryaasen.upgrader', - version: '1.9.6', - buildNumber: '42', - ), - ); + ), + )..installPackageInfo( + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.larryaasen.upgrader', + version: '1.9.6', + buildNumber: '42', + ), + ); - await upgrader.initialize(); + await upgrader.initialize(); - expect(fakeAppcast.callCount, greaterThan(0)); - }, skip: false); + expect(fakeAppcast.callCount, greaterThan(0)); + }, skip: false); - test('will use appcast critical version if exists', () async { - final upgraderOS = MockUpgraderOS(android: true); - final Client mockClient = - setupMockClient(filePath: 'test/testappcast_critical.xml'); + test('will use appcast critical version if exists', () async { + final upgraderOS = MockUpgraderOS(android: true); + final Client mockClient = + setupMockClient(filePath: 'test/testappcast_critical.xml'); - final upgrader = Upgrader( - client: mockClient, - upgraderOS: upgraderOS, - debugLogging: true, - storeController: UpgraderStoreController( - onAndroid: () => UpgraderAppcastStore( - appcastURL: 'https://sparkle-project.org/test/testappcast.xml'), + final upgrader = Upgrader( + client: mockClient, + upgraderOS: upgraderOS, + upgraderDevice: MockUpgraderDevice(), + debugLogging: true, + storeController: UpgraderStoreController( + onAndroid: () => UpgraderAppcastStore( + appcastURL: 'https://sparkle-project.org/test/testappcast.xml', + // client: mockClient, ), - )..installPackageInfo( - packageInfo: PackageInfo( - appName: 'Upgrader', - packageName: 'com.larryaasen.upgrader', - version: '1.9.6', - buildNumber: '42', - ), - ); - - await upgrader.initialize(); - - var notCalled = true; - upgrader.willDisplayUpgrade = ({ - required bool display, - String? installedVersion, - required UpgraderVersionInfo versionInfo, - }) { - expect(display, true); - expect(installedVersion, '1.9.6'); - - /// Appcast Test critical version. - expect(versionInfo.appStoreVersion, '3.0.0'); - notCalled = false; - }; + ), + )..installPackageInfo( + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.larryaasen.upgrader', + version: '1.9.6', + buildNumber: '42', + ), + ); - final shouldDisplayUpgrade = upgrader.shouldDisplayUpgrade(); + await upgrader.initialize(); - expect(shouldDisplayUpgrade, isTrue); - expect(notCalled, false); - }, skip: false); + var notCalled = true; + upgrader.willDisplayUpgrade = ({ + required bool display, + String? installedVersion, + UpgraderVersionInfo? versionInfo, + }) { + expect(display, true); + expect(installedVersion, '1.9.6'); + + /// Appcast Test critical version. + expect(versionInfo!.appStoreVersion.toString(), '3.0.0'); + notCalled = false; + }; + + final shouldDisplayUpgrade = upgrader.shouldDisplayUpgrade(); + + expect(shouldDisplayUpgrade, isTrue); + expect(notCalled, false); + }, skip: false); - test('will use appcast last item', () async { - final upgraderOS = MockUpgraderOS(ios: true); + test('will use appcast last item', () async { + final upgraderOS = MockUpgraderOS(ios: true); - final Client mockClient = - setupMockClient(filePath: 'test/testappcastmulti.xml'); + final Client mockClient = + setupMockClient(filePath: 'test/testappcastmulti.xml'); - final upgrader = Upgrader( - client: mockClient, - upgraderOS: upgraderOS, - debugLogging: true, - storeController: UpgraderStoreController( - oniOS: () => UpgraderAppcastStore( - appcastURL: 'https://sparkle-project.org/test/testappcast.xml'), + final upgrader = Upgrader( + client: mockClient, + upgraderOS: upgraderOS, + upgraderDevice: MockUpgraderDevice(), + debugLogging: true, + storeController: UpgraderStoreController( + oniOS: () => UpgraderAppcastStore( + appcastURL: 'https://sparkle-project.org/test/testappcast.xml'), + ), + )..installPackageInfo( + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.larryaasen.upgrader', + version: '1.9.6', + buildNumber: '42', ), - )..installPackageInfo( - packageInfo: PackageInfo( - appName: 'Upgrader', - packageName: 'com.larryaasen.upgrader', - version: '1.9.6', - buildNumber: '42', - ), - ); - - await upgrader.initialize(); - - var notCalled = true; - upgrader.willDisplayUpgrade = ({ - required bool display, - String? installedVersion, - required UpgraderVersionInfo versionInfo, - }) { - expect(display, true); - expect(installedVersion, '1.9.6'); - expect(versionInfo.appStoreVersion, '2.3.2'); - notCalled = false; - }; + ); - final shouldDisplayUpgrade = upgrader.shouldDisplayUpgrade(); + await upgrader.initialize(); - expect(shouldDisplayUpgrade, isTrue); - expect(notCalled, false); - }, skip: false); + var notCalled = true; + upgrader.willDisplayUpgrade = ({ + required bool display, + String? installedVersion, + UpgraderVersionInfo? versionInfo, + }) { + expect(display, true); + expect(installedVersion, '1.9.6'); + expect(versionInfo!.appStoreVersion.toString(), '2.3.2'); + notCalled = false; + }; + + final shouldDisplayUpgrade = upgrader.shouldDisplayUpgrade(); + + expect(shouldDisplayUpgrade, isTrue); + expect(notCalled, false); + }, skip: false); - test('durationUntilAlertAgain defaults to 3 days', () async { - final upgrader = Upgrader(); - expect(upgrader.durationUntilAlertAgain, const Duration(days: 3)); - }, skip: false); + test('durationUntilAlertAgain defaults to 3 days', () async { + final upgrader = Upgrader(); + expect(upgrader.durationUntilAlertAgain, const Duration(days: 3)); + }, skip: false); - test('durationUntilAlertAgain is 0 days', () async { - final upgrader = - Upgrader(durationUntilAlertAgain: const Duration(seconds: 0)); - expect(upgrader.durationUntilAlertAgain, const Duration(seconds: 0)); + test('durationUntilAlertAgain is 0 days', () async { + final upgrader = + Upgrader(durationUntilAlertAgain: const Duration(seconds: 0)); + expect(upgrader.durationUntilAlertAgain, const Duration(seconds: 0)); - UpgradeAlert(upgrader: upgrader); - expect(upgrader.durationUntilAlertAgain, const Duration(seconds: 0)); + UpgradeAlert(upgrader: upgrader); + expect(upgrader.durationUntilAlertAgain, const Duration(seconds: 0)); - UpgradeCard(upgrader: upgrader); - expect(upgrader.durationUntilAlertAgain, const Duration(seconds: 0)); - }, skip: false); + UpgradeCard(upgrader: upgrader); + expect(upgrader.durationUntilAlertAgain, const Duration(seconds: 0)); + }, skip: false); - test('durationUntilAlertAgain card is valid', () async { - final upgrader = - Upgrader(durationUntilAlertAgain: const Duration(days: 3)); - UpgradeCard(upgrader: upgrader); - expect(upgrader.durationUntilAlertAgain, const Duration(days: 3)); + test('durationUntilAlertAgain card is valid', () async { + final upgrader = Upgrader(durationUntilAlertAgain: const Duration(days: 3)); + UpgradeCard(upgrader: upgrader); + expect(upgrader.durationUntilAlertAgain, const Duration(days: 3)); - final upgrader2 = - Upgrader(durationUntilAlertAgain: const Duration(days: 10)); - UpgradeCard(upgrader: upgrader2); - expect(upgrader2.durationUntilAlertAgain, const Duration(days: 10)); - }, skip: false); + final upgrader2 = + Upgrader(durationUntilAlertAgain: const Duration(days: 10)); + UpgradeCard(upgrader: upgrader2); + expect(upgrader2.durationUntilAlertAgain, const Duration(days: 10)); + }, skip: false); - test('durationUntilAlertAgain alert is valid', () async { - final upgrader = - Upgrader(durationUntilAlertAgain: const Duration(days: 3)); - UpgradeAlert(upgrader: upgrader); - expect(upgrader.durationUntilAlertAgain, const Duration(days: 3)); + test('durationUntilAlertAgain alert is valid', () async { + final upgrader = Upgrader(durationUntilAlertAgain: const Duration(days: 3)); + UpgradeAlert(upgrader: upgrader); + expect(upgrader.durationUntilAlertAgain, const Duration(days: 3)); - final upgrader2 = - Upgrader(durationUntilAlertAgain: const Duration(days: 10)); - UpgradeAlert(upgrader: upgrader2); - expect(upgrader2.durationUntilAlertAgain, const Duration(days: 10)); - }, skip: false); - }); + final upgrader2 = + Upgrader(durationUntilAlertAgain: const Duration(days: 10)); + UpgradeAlert(upgrader: upgrader2); + expect(upgrader2.durationUntilAlertAgain, const Duration(days: 10)); + }, skip: false); group('shouldDisplayUpgrade', () { - test('should respect debugDisplayAlways property', () { + test('should respect debugDisplayAlways property', () async { final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader( + upgraderDevice: MockUpgraderDevice(), upgraderOS: MockUpgraderOS(ios: true), client: client, debugLogging: true); expect(upgrader.shouldDisplayUpgrade(), false); - upgrader.debugDisplayAlways = true; + upgrader.updateState(upgrader.state.copyWith(debugDisplayAlways: true)); expect(upgrader.shouldDisplayUpgrade(), true); - upgrader.debugDisplayAlways = false; + upgrader.updateState(upgrader.state.copyWith(debugDisplayAlways: false)); expect(upgrader.shouldDisplayUpgrade(), false); // Test the willDisplayUpgrade callback @@ -849,28 +848,28 @@ void main() { upgrader.willDisplayUpgrade = ({ required bool display, String? installedVersion, - required UpgraderVersionInfo versionInfo, + UpgraderVersionInfo? versionInfo, }) { expect(display, false); - expect(versionInfo.minAppVersion, isNull); + expect(versionInfo?.minAppVersion, isNull); expect(installedVersion, isNull); - expect(versionInfo.appStoreVersion, isNull); + expect(versionInfo?.appStoreVersion, isNull); notCalled = false; }; expect(upgrader.shouldDisplayUpgrade(), false); expect(notCalled, false); - upgrader.debugDisplayAlways = true; + upgrader.updateState(upgrader.state.copyWith(debugDisplayAlways: true)); notCalled = true; upgrader.willDisplayUpgrade = ({ required bool display, String? installedVersion, - required UpgraderVersionInfo versionInfo, + UpgraderVersionInfo? versionInfo, }) { expect(display, true); - expect(versionInfo.minAppVersion, isNull); + expect(versionInfo?.minAppVersion, isNull); expect(installedVersion, isNull); - expect(versionInfo.appStoreVersion, isNull); + expect(versionInfo?.appStoreVersion, isNull); notCalled = false; }; expect(upgrader.shouldDisplayUpgrade(), true); @@ -897,13 +896,13 @@ void main() { upgrader.willDisplayUpgrade = ({ required bool display, String? installedVersion, - required UpgraderVersionInfo versionInfo, + UpgraderVersionInfo? versionInfo, }) { expect(display, true); - expect(versionInfo.minAppVersion, '2.0.0'); + expect(versionInfo!.minAppVersion, isNull); expect(upgrader.minAppVersion, '2.0.0'); expect(installedVersion, '1.9.6'); - expect(versionInfo.appStoreVersion, '5.6.0'); + expect(versionInfo.appStoreVersion.toString(), '5.6.0'); notCalled = false; }; From e791156b276ad70e2e1be59c5428cbfe888074fc Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sat, 3 Feb 2024 21:55:46 -0500 Subject: [PATCH 05/15] Code cleanup. --- lib/src/upgrade_messages.dart | 10 +- lib/src/upgrade_state.dart | 20 ++- lib/src/upgrader.dart | 318 ++++++++++++++++++---------------- test/upgrade_card_test.dart | 32 ++-- test/upgrader_test.dart | 164 ++++++++++-------- 5 files changed, 291 insertions(+), 253 deletions(-) diff --git a/lib/src/upgrade_messages.dart b/lib/src/upgrade_messages.dart index 0b03a731..d5005361 100644 --- a/lib/src/upgrade_messages.dart +++ b/lib/src/upgrade_messages.dart @@ -88,13 +88,11 @@ class UpgraderMessages { Locale? locale; if (context != null) { locale = Localizations.maybeLocaleOf(context); - } else { - // Get the system locale - locale = PlatformDispatcher.instance.locale; } - final code = locale == null || locale.languageCode.isEmpty - ? 'en' - : locale.languageCode; + // Get the system locale + locale ??= PlatformDispatcher.instance.locale; + + final code = locale.languageCode.isEmpty ? 'en' : locale.languageCode; return code; } diff --git a/lib/src/upgrade_state.dart b/lib/src/upgrade_state.dart index 2ae5af69..f8a1b1ff 100644 --- a/lib/src/upgrade_state.dart +++ b/lib/src/upgrade_state.dart @@ -5,6 +5,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:version/version.dart'; import 'upgrade_device.dart'; +import 'upgrade_messages.dart'; import 'upgrade_os.dart'; import 'upgrader_version_info.dart'; @@ -17,7 +18,9 @@ class UpgraderState { this.debugDisplayAlways = false, this.debugDisplayOnce = false, this.debugLogging = false, + this.durationUntilAlertAgain = const Duration(days: 3), this.languageCodeOverride, + this.messages, this.minAppVersion, this.packageInfo, required this.upgraderDevice, @@ -40,9 +43,16 @@ class UpgraderState { /// Enable print statements for debugging. final bool debugLogging; - /// The country code that will override the system locale. Optional. Used only for Android. + /// Duration until alerting user again. + final Duration durationUntilAlertAgain; + + /// The country code that will override the system locale. Optional. Used + /// only for Android. final String? languageCodeOverride; + /// The localized messages used for display in upgrader. + final UpgraderMessages? messages; + /// The minimum app version supported by this app. Earlier versions of this app /// will be forced to update to the current version. Optional. final Version? minAppVersion; @@ -67,7 +77,9 @@ class UpgraderState { bool? debugDisplayAlways, bool? debugDisplayOnce, bool? debugLogging, + Duration? durationUntilAlertAgain, String? languageCodeOverride, + UpgraderMessages? messages, Version? minAppVersion, PackageInfo? packageInfo, UpgraderDevice? upgraderDevice, @@ -80,7 +92,10 @@ class UpgraderState { debugDisplayAlways: debugDisplayAlways ?? this.debugDisplayAlways, debugDisplayOnce: debugDisplayOnce ?? this.debugDisplayOnce, debugLogging: debugLogging ?? this.debugLogging, + durationUntilAlertAgain: + durationUntilAlertAgain ?? this.durationUntilAlertAgain, languageCodeOverride: languageCodeOverride ?? this.languageCodeOverride, + messages: messages ?? this.messages, minAppVersion: minAppVersion ?? this.minAppVersion, packageInfo: packageInfo ?? this.packageInfo, upgraderDevice: upgraderDevice ?? this.upgraderDevice, @@ -94,6 +109,7 @@ class UpgraderState { UpgraderState copyWithNull({ bool? countryCodeOverride, bool? languageCodeOverride, + bool? messages, bool? minAppVersion, bool? packageInfo, bool? versionInfo, @@ -105,8 +121,10 @@ class UpgraderState { debugDisplayAlways: debugDisplayAlways, debugDisplayOnce: debugDisplayOnce, debugLogging: debugLogging, + durationUntilAlertAgain: durationUntilAlertAgain, languageCodeOverride: languageCodeOverride == true ? null : this.languageCodeOverride, + messages: messages == true ? null : this.messages, minAppVersion: minAppVersion == true ? null : this.minAppVersion, packageInfo: packageInfo == true ? null : this.packageInfo, upgraderDevice: upgraderDevice, diff --git a/lib/src/upgrader.dart b/lib/src/upgrader.dart index 0e150738..1240b205 100644 --- a/lib/src/upgrader.dart +++ b/lib/src/upgrader.dart @@ -34,18 +34,26 @@ typedef WillDisplayUpgradeCallback = void Function({ /// Creates a shared instance of [Upgrader]. Upgrader _sharedInstance = Upgrader(); -/// A class to configure the upgrade dialog. +/// An upgrade controllerthat maintains a [state] that is used to +/// trigger an alert or other UI to evaluate upgrading criteria. +/// +/// See also: +/// +/// * [UpgraderMessages], the default localized messages used for display. +/// * [UpgraderState], the [Upgrader] state. class Upgrader with WidgetsBindingObserver { + /// Creates an uprade controller that maintains a [state] that is used to + /// trigger an alert or other UI to evaluate upgrading criteria. Upgrader({ - this.messages, bool debugDisplayAlways = false, bool debugDisplayOnce = false, bool debugLogging = false, - this.durationUntilAlertAgain = const Duration(days: 3), + Duration durationUntilAlertAgain = const Duration(days: 3), this.willDisplayUpgrade, http.Client? client, String? countryCode, String? languageCode, + UpgraderMessages? messages, String? minAppVersion, UpgraderStoreController? storeController, UpgraderDevice? upgraderDevice, @@ -56,28 +64,22 @@ class Upgrader with WidgetsBindingObserver { debugDisplayAlways: debugDisplayAlways, debugDisplayOnce: debugDisplayOnce, debugLogging: debugLogging, + durationUntilAlertAgain: durationUntilAlertAgain, languageCodeOverride: languageCode, + messages: messages, minAppVersion: - _parseVersion(minAppVersion, 'minAppVersion', debugLogging), + parseVersion(minAppVersion, 'minAppVersion', debugLogging), upgraderDevice: upgraderDevice ?? UpgraderDevice(), upgraderOS: upgraderOS ?? UpgraderOS(), ), storeController = storeController ?? UpgraderStoreController() { - if (debugLogging) print("upgrader: instantiated"); + if (_state.debugLogging) { + print("upgrader: instantiated"); + } } - /// The [Upgrader] state. - UpgraderState _state; - UpgraderState get state => _state; - /// The controller that provides the store details for each platform. - final UpgraderStoreController storeController; - - /// Duration until alerting user again - final Duration durationUntilAlertAgain; - - /// The localized messages used for display in upgrader. - UpgraderMessages? messages; + UpgraderStoreController storeController; /// Called when [Upgrader] determines that an upgrade may or may not be /// displayed. The [value] parameter will be true when it should be displayed, @@ -85,78 +87,31 @@ class Upgrader with WidgetsBindingObserver { /// is logging metrics for your app. WillDisplayUpgradeCallback? willDisplayUpgrade; - bool _initCalled = false; - String? _installedVersion; - Version? _updateAvailable; - DateTime? _lastTimeAlerted; - Version? _lastVersionAlerted; - Version? _userIgnoredVersion; - bool _hasAlerted = false; + /// A shared instance of [Upgrader]. + static Upgrader get sharedInstance => _sharedInstance; - /// Track the initialization future so that [initialize] can be called multiple times. - Future? _futureInit; + /// The [Upgrader] state. + UpgraderState _state; + UpgraderState get state => _state; /// A stream that provides a new state each time an evaluation should be performed. /// The values will always be the state. Stream get stateStream => _streamController.stream; final _streamController = StreamController.broadcast(); - /// A shared instance of [Upgrader]. - static Upgrader get sharedInstance => _sharedInstance; + /// Track the initialization future so that [initialize] can be called multiple times. + Future? _futureInit; + + bool _initCalled = false; + Version? _updateAvailable; + DateTime? _lastTimeAlerted; + Version? _lastVersionAlerted; + Version? _userIgnoredVersion; + bool _hasAlerted = false; static const notInitializedExceptionMessage = 'upgrader: initialize() not called. Must be called first.'; - String? get currentAppStoreListingURL => - state.versionInfo?.appStoreListingURL; - - String? get currentAppStoreVersion => - state.versionInfo?.appStoreVersion?.toString(); - - String? get currentInstalledVersion => _installedVersion; - - String? get releaseNotes => state.versionInfo?.releaseNotes; - - void installPackageInfo({PackageInfo? packageInfo}) { - updateState(state.copyWith(packageInfo: packageInfo)); - _initCalled = false; - } - - /// The minAppVersion in the Upgrader state. - String? get minAppVersion => state.minAppVersion.toString(); - - set minAppVersion(String? version) { - if (version == null) { - updateState(state.copyWithNull(minAppVersion: true)); - } else { - final parsedVersion = - _parseVersion(version, 'minAppVersion', state.debugLogging); - if (parsedVersion != null) { - updateState(state.copyWith(minAppVersion: parsedVersion)); - } - } - } - - static Version? _parseVersion( - String? version, String name, bool debugLogging) { - if (version == null) return null; - try { - return Version.parse(version); - } catch (e) { - // if (state.debugLogging) { - print('upgrader: _parseVersion $name exception: $e'); - // } - return null; - } - } - - // void installAppStoreVersion(String version) => _appStoreVersion = version; - - // void installAppStoreListingURL(String url) => _appStoreListingURL = url; - - /// The latest version info for this app. - UpgraderVersionInfo? get versionInfo => state.versionInfo; - /// Initialize [Upgrader] by getting saved preferences, getting platform package info, and getting /// released version info. Future initialize() async { @@ -190,9 +145,7 @@ class Upgrader with WidgetsBindingObserver { } } - _installedVersion = state.packageInfo!.version; - - updateState(state.copyWith(versionInfo: await updateVersionInfo())); + await updateVersionInfo(); // Add an observer of application events, so that when the app returns // from the background, the version info is updated. @@ -203,9 +156,18 @@ class Upgrader with WidgetsBindingObserver { return _futureInit!; } - /// Updates the Upgrader state. - void updateState(UpgraderState newState) { + /// Updates the Upgrader state, which updates the stream, which triggers a + /// call to [shouldDisplayUpgrade]. + void updateState(UpgraderState newState, + {bool updateTheVersionInfo = false}) { _state = newState; + + if (updateTheVersionInfo) { + Future.delayed(Duration.zero).then((value) async { + await updateVersionInfo(); + }); + return; + } updateStream(); } @@ -229,50 +191,62 @@ class Upgrader with WidgetsBindingObserver { // When app has resumed from background. if (lifecycleState == AppLifecycleState.resumed) { - updateState(state.copyWith(versionInfo: await updateVersionInfo())); + await updateVersionInfo(); } } - /// Update the version info for this app. + /// Update the version info for this app by using an [UpgraderStore] to get + /// the [UpgraderVersionInfo]. Future updateVersionInfo() async { if (state.packageInfo == null || state.packageInfo!.packageName.isEmpty) { + updateState(state.copyWithNull(versionInfo: null)); return null; } // Determine the store to be used for this app. final store = storeController.getUpgraderStore(state.upgraderOS); - if (store == null) return null; + if (store == null) { + updateState(state.copyWithNull(versionInfo: null)); + return null; + } // Determine the installed version of this app. late Version installedVersion; try { - installedVersion = Version.parse(_installedVersion!); + installedVersion = Version.parse(state.packageInfo!.version); } catch (e) { if (state.debugLogging) { print('upgrader: installedVersion exception: $e'); - return null; } + updateState(state.copyWithNull(versionInfo: null)); + return null; } + final locale = findLocale(); + // Determine the country code of the locale, defaulting to `US`. - final country = state.countryCodeOverride ?? findCountryCode(); + final country = + state.countryCodeOverride ?? findCountryCode(locale: locale); if (state.debugLogging) { print('upgrader: countryCode: $country'); } // Determine the language code of the locale, defaulting to `en`. - final language = state.languageCodeOverride ?? findLanguageCode(); + final language = + state.languageCodeOverride ?? findLanguageCode(locale: locale); if (state.debugLogging) { print('upgrader: languageCode: $language'); } // Get the version info from the store. - final versionInfo = store.getVersionInfo( + final versionInfo = await store.getVersionInfo( state: state, installedVersion: installedVersion, country: country, language: language); + updateState(state.copyWith(versionInfo: versionInfo)); + return versionInfo; } @@ -284,7 +258,7 @@ class Upgrader with WidgetsBindingObserver { bool verifyInit() { if (!_initCalled) { - throw ('upgrader: initialize() not called. Must be called first.'); + throw (notInitializedExceptionMessage); } return true; } @@ -304,40 +278,6 @@ class Upgrader with WidgetsBindingObserver { return msg; } - /// Determine which [UpgraderMessages] object to use. It will be either the one passed - /// to [Upgrader], or one based on the app locale. - UpgraderMessages determineMessages(BuildContext context) { - { - late UpgraderMessages appMessages; - if (messages != null) { - appMessages = messages!; - } else { - String? languageCode; - try { - // Get the current locale in the app. - final locale = Localizations.localeOf(context); - // Get the current language code in the app. - languageCode = locale.languageCode; - if (state.debugLogging) { - print('upgrader: current locale: $locale'); - } - } catch (e) { - // ignored, really. - } - - appMessages = UpgraderMessages(code: languageCode); - } - - if (appMessages.languageCode.isEmpty) { - print('upgrader: error -> languageCode is empty'); - } else if (state.debugLogging) { - print('upgrader: languageCode: ${appMessages.languageCode}'); - } - - return appMessages; - } - } - bool blocked() { return belowMinAppVersion() || versionInfo?.isCriticalUpdate == true; } @@ -370,7 +310,7 @@ class Upgrader with WidgetsBindingObserver { if (willDisplayUpgrade != null) { willDisplayUpgrade!( display: rv, - installedVersion: _installedVersion, + installedVersion: state.packageInfo?.version, versionInfo: versionInfo, ); } @@ -382,9 +322,9 @@ class Upgrader with WidgetsBindingObserver { bool belowMinAppVersion() { var rv = false; final minVersion = state.minAppVersion ?? versionInfo?.minAppVersion; - if (minVersion != null) { + if (minVersion != null && state.packageInfo != null) { try { - final installedVersion = Version.parse(_installedVersion!); + final installedVersion = Version.parse(state.packageInfo!.version); rv = installedVersion < minVersion; } catch (e) { if (state.debugLogging) { @@ -401,7 +341,7 @@ class Upgrader with WidgetsBindingObserver { } final lastAlertedDuration = DateTime.now().difference(_lastTimeAlerted!); - final rv = lastAlertedDuration < durationUntilAlertAgain; + final rv = lastAlertedDuration < state.durationUntilAlertAgain; if (rv && state.debugLogging) { print('upgrader: isTooSoon: true'); } @@ -419,16 +359,17 @@ class Upgrader with WidgetsBindingObserver { bool isUpdateAvailable() { if (state.debugLogging) { - print('upgrader: installedVersion: $_installedVersion'); + print('upgrader: installedVersion: ${state.packageInfo?.version}'); print('upgrader: minAppVersion: ${state.minAppVersion}'); } - if (versionInfo?.appStoreVersion == null || _installedVersion == null) { + if (versionInfo?.appStoreVersion == null || + state.packageInfo?.version == null) { if (state.debugLogging) print('upgrader: isUpdateAvailable: false'); return false; } try { - final installedVersion = Version.parse(_installedVersion!); + final installedVersion = Version.parse(state.packageInfo!.version); final available = versionInfo!.appStoreVersion! > installedVersion; _updateAvailable = available ? versionInfo?.appStoreVersion : null; @@ -442,35 +383,31 @@ class Upgrader with WidgetsBindingObserver { return isAvailable; } - /// Determine the current country code, either from the context, or - /// from the system-reported default locale of the device. The default - /// is `US`. - String? findCountryCode({BuildContext? context}) { + Locale findLocale({BuildContext? context}) { Locale? locale; if (context != null) { locale = Localizations.maybeLocaleOf(context); - } else { - // Get the system locale - locale = PlatformDispatcher.instance.locale; } - final code = locale == null || locale.countryCode == null - ? 'US' - : locale.countryCode; + locale ??= PlatformDispatcher.instance.locale; + if (state.debugLogging) { + print('upgrader: current locale: $locale'); + } + return locale; + } + + /// Determine the current country code, either from the context, or + /// from the system-reported default locale of the device. The default + /// is `US`. + String? findCountryCode({required Locale locale}) { + final code = locale.countryCode ?? 'US'; return code; } /// Determine the current language code, either from the context, or /// from the system-reported default locale of the device. The default /// is `en`. - String? findLanguageCode({BuildContext? context}) { - Locale? locale; - if (context != null) { - locale = Localizations.maybeLocaleOf(context); - } else { - // Get the system locale - locale = PlatformDispatcher.instance.locale; - } - final code = locale == null ? 'en' : locale.languageCode; + String? findLanguageCode({required Locale locale}) { + final code = locale.languageCode; return code; } @@ -483,6 +420,28 @@ class Upgrader with WidgetsBindingObserver { return; } + /// Determine which [UpgraderMessages] object to use. It will be either the one passed + /// to [Upgrader], or one based on the app locale. + UpgraderMessages determineMessages(BuildContext context) { + if (state.messages != null) return state.messages!; + + String? languageCode = state.languageCodeOverride; + if (languageCode == null) { + final locale = findLocale(context: context); + languageCode = locale.languageCode; + } + + final appMessages = UpgraderMessages(code: languageCode); + + if (appMessages.languageCode.isEmpty) { + print('upgrader: error -> languageCode is empty'); + } else if (state.debugLogging) { + print('upgrader: languageCode: ${appMessages.languageCode}'); + } + + return appMessages; + } + Future saveIgnored() async { var prefs = await SharedPreferences.getInstance(); @@ -511,7 +470,6 @@ class Upgrader with WidgetsBindingObserver { if (lastTimeAlerted != null) { _lastTimeAlerted = DateTime.parse(lastTimeAlerted); } - final versionAlerted = prefs.getString('lastVersionAlerted'); if (versionAlerted != null) { try { @@ -536,6 +494,7 @@ class Upgrader with WidgetsBindingObserver { return true; } + /// Launch the app store from the app store listing URL. void sendUserToAppStore() async { final appStoreListingURL = versionInfo?.appStoreListingURL; if (appStoreListingURL == null || appStoreListingURL.isEmpty) { @@ -560,6 +519,59 @@ class Upgrader with WidgetsBindingObserver { print('upgrader: launch to app store failed: $e'); } } - } else {} + } } + + static Version? parseVersion( + String? version, String name, bool debugLogging) { + if (version == null) return null; + try { + return Version.parse(version); + } catch (e) { + // if (state.debugLogging) { + print('upgrader: _parseVersion $name exception: $e'); + // } + return null; + } + } +} + +extension UpgraderExt on Upgrader { + String? get currentAppStoreListingURL => + state.versionInfo?.appStoreListingURL; + + String? get currentAppStoreVersion => + state.versionInfo?.appStoreVersion?.toString(); + + String? get currentInstalledVersion => state.packageInfo?.version; + + String? get releaseNotes => state.versionInfo?.releaseNotes; + + void installPackageInfo({PackageInfo? packageInfo}) { + updateState(state.copyWith(packageInfo: packageInfo), + updateTheVersionInfo: true); + } + + /// The minAppVersion in the Upgrader state. + String? get minAppVersion => state.minAppVersion.toString(); + + set minAppVersion(String? version) { + if (version == null) { + updateState( + state.copyWithNull( + minAppVersion: true, + ), + updateTheVersionInfo: true); + } else { + final parsedVersion = + Upgrader.parseVersion(version, 'minAppVersion', state.debugLogging); + if (parsedVersion != null) { + updateState(state.copyWith(minAppVersion: parsedVersion), + updateTheVersionInfo: true); + } + } + } + + /// The latest version info for this app. + UpgraderVersionInfo? get versionInfo => state.versionInfo; } diff --git a/test/upgrade_card_test.dart b/test/upgrade_card_test.dart index d84c7f09..8ee71f43 100644 --- a/test/upgrade_card_test.dart +++ b/test/upgrade_card_test.dart @@ -77,18 +77,18 @@ void main() { // Pump the UI so the upgrade card is displayed await tester.pumpAndSettle(); - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); + expect(upgrader.state.messages, isNull); + upgrader.updateState(upgrader.state.copyWith(messages: UpgraderMessages())); + expect(upgrader.state.messages, isNotNull); - expect(find.text(upgrader.messages!.releaseNotes), findsOneWidget); + expect(find.text(upgrader.state.messages!.releaseNotes), findsOneWidget); expect(find.text(upgrader.releaseNotes!), findsOneWidget); - await tester.tap(find.text(upgrader.messages!.buttonTitleUpdate)); + await tester.tap(find.text(upgrader.state.messages!.buttonTitleUpdate)); await tester.pumpAndSettle(); expect(called, true); expect(notCalled, true); - expect(find.text(upgrader.messages!.buttonTitleUpdate), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleUpdate), findsNothing); }, skip: false); testWidgets('test UpgradeCard ignore', (WidgetTester tester) async { @@ -132,16 +132,16 @@ void main() { // Pump the UI so the upgrade card is displayed await tester.pumpAndSettle(); - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); + expect(upgrader.state.messages, isNull); + upgrader.updateState(upgrader.state.copyWith(messages: UpgraderMessages())); + expect(upgrader.state.messages, isNotNull); - await tester.tap(find.text(upgrader.messages!.buttonTitleIgnore)); + await tester.tap(find.text(upgrader.state.messages!.buttonTitleIgnore)); await tester.pumpAndSettle(); expect(called, true); expect(notCalled, true); - expect(find.text(upgrader.messages!.buttonTitleIgnore), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleIgnore), findsNothing); }, skip: false); testWidgets('test UpgradeCard later', (WidgetTester tester) async { @@ -185,15 +185,15 @@ void main() { // Pump the UI so the upgrade card is displayed await tester.pumpAndSettle(const Duration(milliseconds: 5000)); - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); + expect(upgrader.state.messages, isNull); + upgrader.updateState(upgrader.state.copyWith(messages: UpgraderMessages())); + expect(upgrader.state.messages, isNotNull); - await tester.tap(find.text(upgrader.messages!.buttonTitleLater)); + await tester.tap(find.text(upgrader.state.messages!.buttonTitleLater)); await tester.pumpAndSettle(); expect(called, true); expect(notCalled, true); - expect(find.text(upgrader.messages!.buttonTitleLater), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleLater), findsNothing); }, skip: false); } diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 67e72c88..42b097bd 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -180,21 +180,23 @@ void main() { expect(upgrader.isUpdateAvailable(), true); expect(upgrader.isTooSoon(), false); - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); + expect(upgrader.state.messages, isNull); + upgrader.updateState(upgrader.state.copyWith(messages: UpgraderMessages())); + upgrader.updateState(upgrader.state.copyWith(messages: UpgraderMessages())); + expect(upgrader.state.messages, isNotNull); - expect(upgrader.messages?.buttonTitleIgnore, 'IGNORE'); - expect(upgrader.messages?.buttonTitleLater, 'LATER'); - expect(upgrader.messages?.buttonTitleUpdate, 'UPDATE NOW'); - expect(upgrader.messages?.releaseNotes, 'Release Notes'); + expect(upgrader.state.messages?.buttonTitleIgnore, 'IGNORE'); + expect(upgrader.state.messages?.buttonTitleLater, 'LATER'); + expect(upgrader.state.messages?.buttonTitleUpdate, 'UPDATE NOW'); + expect(upgrader.state.messages?.releaseNotes, 'Release Notes'); - upgrader.messages = MyUpgraderMessages(); + upgrader + .updateState(upgrader.state.copyWith(messages: MyUpgraderMessages())); - expect(upgrader.messages!.buttonTitleIgnore, 'aaa'); - expect(upgrader.messages!.buttonTitleLater, 'bbb'); - expect(upgrader.messages!.buttonTitleUpdate, 'ccc'); - expect(upgrader.messages!.releaseNotes, 'ddd'); + expect(upgrader.state.messages!.buttonTitleIgnore, 'aaa'); + expect(upgrader.state.messages!.buttonTitleLater, 'bbb'); + expect(upgrader.state.messages!.buttonTitleUpdate, 'ccc'); + expect(upgrader.state.messages!.releaseNotes, 'ddd'); var called = false; var notCalled = true; @@ -229,24 +231,27 @@ void main() { expect(upgrader.isTooSoon(), true); - expect(find.text(upgrader.messages!.title), findsOneWidget); - expect(find.text(upgrader.body(upgrader.messages!)), findsOneWidget); - expect(find.text(upgrader.messages!.releaseNotes), findsOneWidget); + expect(find.text(upgrader.state.messages!.title), findsOneWidget); + expect(find.text(upgrader.body(upgrader.state.messages!)), findsOneWidget); + expect(find.text(upgrader.state.messages!.releaseNotes), findsOneWidget); expect(find.text(upgrader.releaseNotes!), findsOneWidget); - expect(find.text(upgrader.messages!.prompt), findsOneWidget); + expect(find.text(upgrader.state.messages!.prompt), findsOneWidget); expect(find.byType(TextButton), findsNWidgets(3)); - expect(find.text(upgrader.messages!.buttonTitleIgnore), findsOneWidget); - expect(find.text(upgrader.messages!.buttonTitleLater), findsOneWidget); - expect(find.text(upgrader.messages!.buttonTitleUpdate), findsOneWidget); - expect(find.text(upgrader.messages!.releaseNotes), findsOneWidget); + expect( + find.text(upgrader.state.messages!.buttonTitleIgnore), findsOneWidget); + expect( + find.text(upgrader.state.messages!.buttonTitleLater), findsOneWidget); + expect( + find.text(upgrader.state.messages!.buttonTitleUpdate), findsOneWidget); + expect(find.text(upgrader.state.messages!.releaseNotes), findsOneWidget); expect(find.byKey(dialogKey), findsOneWidget); - await tester.tap(find.text(upgrader.messages!.buttonTitleUpdate)); + await tester.tap(find.text(upgrader.state.messages!.buttonTitleUpdate)); await tester.pumpAndSettle(); - expect(find.text(upgrader.messages!.buttonTitleIgnore), findsNothing); - expect(find.text(upgrader.messages!.buttonTitleLater), findsNothing); - expect(find.text(upgrader.messages!.buttonTitleUpdate), findsNothing); - expect(find.text(upgrader.messages!.releaseNotes), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleIgnore), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleLater), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleUpdate), findsNothing); + expect(find.text(upgrader.state.messages!.releaseNotes), findsNothing); expect(called, true); expect(notCalled, true); // }); @@ -277,19 +282,20 @@ void main() { expect(upgrader.isUpdateAvailable(), true); expect(upgrader.isTooSoon(), false); - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); + expect(upgrader.state.messages, isNull); + upgrader.updateState(upgrader.state.copyWith(messages: UpgraderMessages())); + expect(upgrader.state.messages, isNotNull); - expect(upgrader.messages!.buttonTitleIgnore, 'IGNORE'); - expect(upgrader.messages!.buttonTitleLater, 'LATER'); - expect(upgrader.messages!.buttonTitleUpdate, 'UPDATE NOW'); + expect(upgrader.state.messages!.buttonTitleIgnore, 'IGNORE'); + expect(upgrader.state.messages!.buttonTitleLater, 'LATER'); + expect(upgrader.state.messages!.buttonTitleUpdate, 'UPDATE NOW'); - upgrader.messages = MyUpgraderMessages(); + upgrader + .updateState(upgrader.state.copyWith(messages: MyUpgraderMessages())); - expect(upgrader.messages!.buttonTitleIgnore, 'aaa'); - expect(upgrader.messages!.buttonTitleLater, 'bbb'); - expect(upgrader.messages!.buttonTitleUpdate, 'ccc'); + expect(upgrader.state.messages!.buttonTitleIgnore, 'aaa'); + expect(upgrader.state.messages!.buttonTitleLater, 'bbb'); + expect(upgrader.state.messages!.buttonTitleUpdate, 'ccc'); var called = false; var notCalled = true; @@ -324,11 +330,11 @@ void main() { expect(upgrader.isTooSoon(), true); - expect(find.text(upgrader.messages!.title), findsOneWidget); - expect(find.text(upgrader.body(upgrader.messages!)), findsOneWidget); - expect(find.text(upgrader.messages!.releaseNotes), findsOneWidget); + expect(find.text(upgrader.state.messages!.title), findsOneWidget); + expect(find.text(upgrader.body(upgrader.state.messages!)), findsOneWidget); + expect(find.text(upgrader.state.messages!.releaseNotes), findsOneWidget); expect(find.text(upgrader.releaseNotes!), findsOneWidget); - expect(find.text(upgrader.messages!.prompt), findsOneWidget); + expect(find.text(upgrader.state.messages!.prompt), findsOneWidget); expect(find.byType(CupertinoDialogAction), findsNWidgets(3)); expect( find.byWidgetPredicate((widget) => @@ -336,16 +342,19 @@ void main() { widget.textStyle == cupertinoButtonTextStyle), findsNWidgets(3), ); - expect(find.text(upgrader.messages!.buttonTitleIgnore), findsOneWidget); - expect(find.text(upgrader.messages!.buttonTitleLater), findsOneWidget); - expect(find.text(upgrader.messages!.buttonTitleUpdate), findsOneWidget); + expect( + find.text(upgrader.state.messages!.buttonTitleIgnore), findsOneWidget); + expect( + find.text(upgrader.state.messages!.buttonTitleLater), findsOneWidget); + expect( + find.text(upgrader.state.messages!.buttonTitleUpdate), findsOneWidget); expect(find.byKey(const Key('upgrader_alert_dialog')), findsOneWidget); - await tester.tap(find.text(upgrader.messages!.buttonTitleUpdate)); + await tester.tap(find.text(upgrader.state.messages!.buttonTitleUpdate)); await tester.pumpAndSettle(); - expect(find.text(upgrader.messages!.buttonTitleIgnore), findsNothing); - expect(find.text(upgrader.messages!.buttonTitleLater), findsNothing); - expect(find.text(upgrader.messages!.buttonTitleUpdate), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleIgnore), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleLater), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleUpdate), findsNothing); expect(called, true); expect(notCalled, true); }, skip: false); @@ -369,9 +378,9 @@ void main() { expect(upgrader.isTooSoon(), false); - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); + expect(upgrader.state.messages, isNull); + upgrader.updateState(upgrader.state.copyWith(messages: UpgraderMessages())); + expect(upgrader.state.messages, isNotNull); var called = false; var notCalled = true; @@ -398,9 +407,9 @@ void main() { // Pump the UI so the upgrader can display its dialog await tester.pumpAndSettle(); - await tester.tap(find.text(upgrader.messages!.buttonTitleIgnore)); + await tester.tap(find.text(upgrader.state.messages!.buttonTitleIgnore)); await tester.pumpAndSettle(); - expect(find.text(upgrader.messages!.buttonTitleIgnore), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleIgnore), findsNothing); expect(called, true); expect(notCalled, true); }, skip: false); @@ -424,9 +433,9 @@ void main() { expect(upgrader.isTooSoon(), false); - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); + expect(upgrader.state.messages, isNull); + upgrader.updateState(upgrader.state.copyWith(messages: UpgraderMessages())); + expect(upgrader.state.messages, isNotNull); var called = false; var notCalled = true; @@ -453,9 +462,9 @@ void main() { // Pump the UI so the upgrader can display its dialog await tester.pumpAndSettle(); - await tester.tap(find.text(upgrader.messages!.buttonTitleLater)); + await tester.tap(find.text(upgrader.state.messages!.buttonTitleLater)); await tester.pumpAndSettle(); - expect(find.text(upgrader.messages!.buttonTitleLater), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleLater), findsNothing); expect(called, true); expect(notCalled, true); }, skip: false); @@ -478,9 +487,9 @@ void main() { expect(upgrader.isTooSoon(), false); - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); + expect(upgrader.state.messages, isNull); + upgrader.updateState(upgrader.state.copyWith(messages: UpgraderMessages())); + expect(upgrader.state.messages, isNotNull); var called = false; final upgradeAlert = wrapper( @@ -564,13 +573,14 @@ void main() { // Pump the UI so the upgrade card is displayed await tester.pumpAndSettle(const Duration(milliseconds: 5000)); - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); + expect(upgrader.state.messages, isNull); + upgrader.updateState(upgrader.state.copyWith(messages: UpgraderMessages())); + expect(upgrader.state.messages, isNotNull); - expect(find.text(upgrader.messages!.buttonTitleIgnore), findsNothing); - expect(find.text(upgrader.messages!.buttonTitleLater), findsNothing); - expect(find.text(upgrader.messages!.buttonTitleUpdate), findsOneWidget); + expect(find.text(upgrader.state.messages!.buttonTitleIgnore), findsNothing); + expect(find.text(upgrader.state.messages!.buttonTitleLater), findsNothing); + expect( + find.text(upgrader.state.messages!.buttonTitleUpdate), findsOneWidget); }, skip: false); testWidgets('test upgrader minAppVersion description android', @@ -660,11 +670,11 @@ void main() { // Pump the UI so the upgrade card is displayed await tester.pumpAndSettle(); - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); + expect(upgrader.state.messages, isNull); + upgrader.updateState(upgrader.state.copyWith(messages: UpgraderMessages())); + expect(upgrader.state.messages, isNotNull); - final laterButton = find.text(upgrader.messages!.buttonTitleLater); + final laterButton = find.text(upgrader.state.messages!.buttonTitleLater); expect(laterButton, findsNothing); expect(called, false); @@ -791,41 +801,41 @@ void main() { test('durationUntilAlertAgain defaults to 3 days', () async { final upgrader = Upgrader(); - expect(upgrader.durationUntilAlertAgain, const Duration(days: 3)); + expect(upgrader.state.durationUntilAlertAgain, const Duration(days: 3)); }, skip: false); test('durationUntilAlertAgain is 0 days', () async { final upgrader = Upgrader(durationUntilAlertAgain: const Duration(seconds: 0)); - expect(upgrader.durationUntilAlertAgain, const Duration(seconds: 0)); + expect(upgrader.state.durationUntilAlertAgain, const Duration(seconds: 0)); UpgradeAlert(upgrader: upgrader); - expect(upgrader.durationUntilAlertAgain, const Duration(seconds: 0)); + expect(upgrader.state.durationUntilAlertAgain, const Duration(seconds: 0)); UpgradeCard(upgrader: upgrader); - expect(upgrader.durationUntilAlertAgain, const Duration(seconds: 0)); + expect(upgrader.state.durationUntilAlertAgain, const Duration(seconds: 0)); }, skip: false); test('durationUntilAlertAgain card is valid', () async { final upgrader = Upgrader(durationUntilAlertAgain: const Duration(days: 3)); UpgradeCard(upgrader: upgrader); - expect(upgrader.durationUntilAlertAgain, const Duration(days: 3)); + expect(upgrader.state.durationUntilAlertAgain, const Duration(days: 3)); final upgrader2 = Upgrader(durationUntilAlertAgain: const Duration(days: 10)); UpgradeCard(upgrader: upgrader2); - expect(upgrader2.durationUntilAlertAgain, const Duration(days: 10)); + expect(upgrader2.state.durationUntilAlertAgain, const Duration(days: 10)); }, skip: false); test('durationUntilAlertAgain alert is valid', () async { final upgrader = Upgrader(durationUntilAlertAgain: const Duration(days: 3)); UpgradeAlert(upgrader: upgrader); - expect(upgrader.durationUntilAlertAgain, const Duration(days: 3)); + expect(upgrader.state.durationUntilAlertAgain, const Duration(days: 3)); final upgrader2 = Upgrader(durationUntilAlertAgain: const Duration(days: 10)); UpgradeAlert(upgrader: upgrader2); - expect(upgrader2.durationUntilAlertAgain, const Duration(days: 10)); + expect(upgrader2.state.durationUntilAlertAgain, const Duration(days: 10)); }, skip: false); group('shouldDisplayUpgrade', () { From 484653147d39a08b8df959dc8ff3c60d5c384cf2 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Mon, 12 Feb 2024 22:51:10 -0500 Subject: [PATCH 06/15] Added unit tests for UpgraderVersionInfo. --- ...ce_test.dart => upgrader_device_test.dart} | 0 test/upgrader_version_info_test.dart | 106 ++++++++++++++++++ 2 files changed, 106 insertions(+) rename test/{device_test.dart => upgrader_device_test.dart} (100%) create mode 100644 test/upgrader_version_info_test.dart diff --git a/test/device_test.dart b/test/upgrader_device_test.dart similarity index 100% rename from test/device_test.dart rename to test/upgrader_device_test.dart diff --git a/test/upgrader_version_info_test.dart b/test/upgrader_version_info_test.dart new file mode 100644 index 00000000..cf1e1fdf --- /dev/null +++ b/test/upgrader_version_info_test.dart @@ -0,0 +1,106 @@ +// Copyright (c) 2024 Larry Aasen. All rights reserved. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:upgrader/src/upgrader_version_info.dart'; +import 'package:version/version.dart'; + +void main() { + test('create_instance_with_all_parameters_null', () { + UpgraderVersionInfo versionInfo = UpgraderVersionInfo( + appStoreListingURL: null, + appStoreVersion: null, + installedVersion: null, + isCriticalUpdate: null, + minAppVersion: null, + releaseNotes: null, + ); + + expect(versionInfo.appStoreListingURL, isNull); + expect(versionInfo.appStoreVersion, isNull); + expect(versionInfo.installedVersion, isNull); + expect(versionInfo.isCriticalUpdate, isNull); + expect(versionInfo.minAppVersion, isNull); + expect(versionInfo.releaseNotes, isNull); + }); + + test('create_instance_with_all_parameters_valid', () { + Version appStoreVersion = Version.parse('1.0.0'); + Version installedVersion = Version.parse('1.0.0'); + Version minAppVersion = Version.parse('1.0.0'); + + UpgraderVersionInfo versionInfo = UpgraderVersionInfo( + appStoreListingURL: 'https://example.com', + appStoreVersion: appStoreVersion, + installedVersion: installedVersion, + isCriticalUpdate: true, + minAppVersion: minAppVersion, + releaseNotes: 'New features and bug fixes', + ); + + expect(versionInfo.appStoreListingURL, equals('https://example.com')); + expect(versionInfo.appStoreVersion, equals(appStoreVersion)); + expect(versionInfo.installedVersion, equals(installedVersion)); + expect(versionInfo.isCriticalUpdate, isTrue); + expect(versionInfo.minAppVersion, equals(minAppVersion)); + expect(versionInfo.releaseNotes, equals('New features and bug fixes')); + }); + + test('to_string_with_all_parameters_null', () { + UpgraderVersionInfo versionInfo = UpgraderVersionInfo( + appStoreListingURL: null, + appStoreVersion: null, + installedVersion: null, + isCriticalUpdate: null, + minAppVersion: null, + releaseNotes: null, + ); + + String result = versionInfo.toString(); + + expect( + result, + equals( + 'appStoreListingURL: null, appStoreVersion: null, installedVersion: null, isCriticalUpdate: null, minAppVersion: null, releaseNotes: null')); + }); + test('create_instance_with_one_parameter_null', () { + Version appStoreVersion = Version.parse('1.0.0'); + + UpgraderVersionInfo versionInfo = UpgraderVersionInfo( + appStoreListingURL: null, + appStoreVersion: appStoreVersion, + installedVersion: null, + isCriticalUpdate: null, + minAppVersion: null, + releaseNotes: null, + ); + + expect(versionInfo.appStoreListingURL, isNull); + expect(versionInfo.appStoreVersion, equals(appStoreVersion)); + expect(versionInfo.installedVersion, isNull); + expect(versionInfo.isCriticalUpdate, isNull); + expect(versionInfo.minAppVersion, isNull); + expect(versionInfo.releaseNotes, isNull); + }); + + test('create_instance_with_valid_version_objects', () { + Version appStoreVersion = Version.parse('1.0.0'); + Version installedVersion = Version.parse('1.0.0'); + Version minAppVersion = Version.parse('1.0.0'); + + UpgraderVersionInfo versionInfo = UpgraderVersionInfo( + appStoreListingURL: null, + appStoreVersion: appStoreVersion, + installedVersion: installedVersion, + isCriticalUpdate: null, + minAppVersion: minAppVersion, + releaseNotes: null, + ); + + expect(versionInfo.appStoreListingURL, isNull); + expect(versionInfo.appStoreVersion, equals(appStoreVersion)); + expect(versionInfo.installedVersion, equals(installedVersion)); + expect(versionInfo.isCriticalUpdate, isNull); + expect(versionInfo.minAppVersion, equals(minAppVersion)); + expect(versionInfo.releaseNotes, isNull); + }); +} From b99e2240b8e7aa5c147134a9838120985e12bdc4 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Mon, 12 Feb 2024 23:47:14 -0500 Subject: [PATCH 07/15] Added unit tests for UpgraderStore. --- test/mock_itunes_client.dart | 1 + test/upgrader_store_controller_test.dart | 120 +++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 test/upgrader_store_controller_test.dart diff --git a/test/mock_itunes_client.dart b/test/mock_itunes_client.dart index 99c08558..874e583f 100644 --- a/test/mock_itunes_client.dart +++ b/test/mock_itunes_client.dart @@ -28,6 +28,7 @@ class MockITunesSearchClient { 'bundleId': 'com.google.Maps', 'currency': currency, 'releaseNotes': 'Bug fixes.', + 'trackViewUrl': 'https://example.com/app', if (description.isNotEmpty) 'description': description } ] diff --git a/test/upgrader_store_controller_test.dart b/test/upgrader_store_controller_test.dart new file mode 100644 index 00000000..9bf99446 --- /dev/null +++ b/test/upgrader_store_controller_test.dart @@ -0,0 +1,120 @@ +// Copyright (c) 2024 Larry Aasen. All rights reserved. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:upgrader/upgrader.dart'; +import 'package:version/version.dart'; + +import 'fake_appcast.dart'; +import 'mock_itunes_client.dart'; +import 'mock_play_store_client.dart'; + +void main() { + test('UpgraderAppStore returns UpgraderVersionInfo', () async { + final installedVersion = Version(1, 9, 6); + final state = UpgraderState( + client: MockITunesSearchClient.setupMockClient(), + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.larryaasen.upgrader', + version: installedVersion.toString(), + buildNumber: '42', + ), + upgraderDevice: MockUpgraderDevice(), + upgraderOS: MockUpgraderOS(ios: true), + ); + + final upgraderAppStore = UpgraderAppStore(); + + final versionInfo = await upgraderAppStore.getVersionInfo( + state: state, + installedVersion: installedVersion, + country: 'US', + language: 'en', + ); + + // Assert + expect(versionInfo.appStoreListingURL, 'https://example.com/app'); + expect(versionInfo.appStoreVersion, Version(5, 6, 0)); + expect(versionInfo.installedVersion, installedVersion); + expect(versionInfo.isCriticalUpdate, isNull); + expect(versionInfo.minAppVersion, isNull); + expect(versionInfo.releaseNotes, 'Bug fixes.'); + }); + + test('UpgraderPlayStore returns UpgraderVersionInfo', () async { + final installedVersion = Version(1, 9, 6); + final state = UpgraderState( + client: await MockPlayStoreSearchClient.setupMockClient(), + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.kotoko.express', + version: installedVersion.toString(), + buildNumber: '42', + ), + upgraderDevice: MockUpgraderDevice(), + upgraderOS: MockUpgraderOS(android: true), + ); + + final upgraderPlayStore = UpgraderPlayStore(); + + // Act + final versionInfo = await upgraderPlayStore.getVersionInfo( + state: state, + installedVersion: installedVersion, + country: 'US', + language: 'en', + ); + + // Assert + expect(versionInfo.appStoreListingURL, isNotNull); + expect( + versionInfo.appStoreListingURL!.startsWith( + 'https://play.google.com/store/apps/details?id=com.kotoko.express&gl=US&hl=en&_cb='), + isTrue); + expect(versionInfo.appStoreVersion, Version(1, 23, 0)); + expect(versionInfo.installedVersion, equals(installedVersion)); + expect(versionInfo.isCriticalUpdate, isNull); + expect(versionInfo.minAppVersion, isNull); + expect(versionInfo.releaseNotes, 'Minor updates and improvements.'); + }); + + test('UpgraderAppcastStore returns UpgraderVersionInfo', () async { + final installedVersion = Version.parse('1.9.6'); + final state = UpgraderState( + client: await MockPlayStoreSearchClient.setupMockClient(), + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.kotoko.express', + version: installedVersion.toString(), + buildNumber: '42', + ), + upgraderDevice: MockUpgraderDevice(), + upgraderOS: MockUpgraderOS(android: true), + ); + + const appcastURL = 'https://sparkle-project.org/test/testappcast.xml'; + final fakeAppcast = FakeAppcast(); + + final upgraderAppcastStore = UpgraderAppcastStore( + appcastURL: appcastURL, + appcast: fakeAppcast, + ); + + // Act + final versionInfo = await upgraderAppcastStore.getVersionInfo( + state: state, + installedVersion: installedVersion, + country: 'US', + language: 'en', + ); + + // Assert + expect( + versionInfo.appStoreListingURL, equals('http://some.fakewebsite.com')); + expect(versionInfo.appStoreVersion, equals(Version.parse('1.0.0'))); + expect(versionInfo.installedVersion, installedVersion); + expect(versionInfo.isCriticalUpdate, isNull); + expect(versionInfo.releaseNotes, isNull); + }); +} From 259aa00d3dfe66a32e1ac137344676ac818dfedc Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Tue, 13 Feb 2024 00:15:55 -0500 Subject: [PATCH 08/15] Added more unit tests. --- lib/src/upgrader.dart | 19 +++++++++---------- test/upgrader_os_test.dart | 16 +++++++++++++++- test/upgrader_store_controller_test.dart | 3 +++ test/upgrader_test.dart | 21 +++++++++++++++++++++ 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/lib/src/upgrader.dart b/lib/src/upgrader.dart index 1240b205..9687fb5d 100644 --- a/lib/src/upgrader.dart +++ b/lib/src/upgrader.dart @@ -133,16 +133,15 @@ class Upgrader with WidgetsBindingObserver { if (state.debugLogging) print('upgrader: ${state.upgraderOS}'); if (state.packageInfo == null) { - updateState( - state.copyWith(packageInfo: await PackageInfo.fromPlatform())); - if (state.debugLogging) { - print( - 'upgrader: package info packageName: ${state.packageInfo!.packageName}'); - print( - 'upgrader: package info appName: ${state.packageInfo!.appName}'); - print( - 'upgrader: package info version: ${state.packageInfo!.version}'); - } + final packageInfo = await PackageInfo.fromPlatform(); + updateState(state.copyWith(packageInfo: packageInfo)); + } + + final packageInfo = state.packageInfo; + if (state.debugLogging && packageInfo != null) { + print('upgrader: packageInfo packageName: ${packageInfo.packageName}'); + print('upgrader: packageInfo appName: ${packageInfo.appName}'); + print('upgrader: packageInfo version: ${packageInfo.version}'); } await updateVersionInfo(); diff --git a/test/upgrader_os_test.dart b/test/upgrader_os_test.dart index e7a60353..dca4c55e 100644 --- a/test/upgrader_os_test.dart +++ b/test/upgrader_os_test.dart @@ -118,7 +118,7 @@ void main() { expect(mock3.isWeb, true); }); - test('MockUpgraderOS', () async { + test('MockUpgraderOS current', () async { expect(MockUpgraderOS().current, ''); expect(MockUpgraderOS(android: true).current, 'android'); expect(MockUpgraderOS(fuchsia: true).current, 'fuchsia'); @@ -128,5 +128,19 @@ void main() { expect(MockUpgraderOS(web: true).current, 'web'); expect(MockUpgraderOS(windows: true).current, 'windows'); }); + + test('MockUpgraderOS currentOSType', () async { + expect(MockUpgraderOS().currentOSType, UpgraderOSType.android); + expect( + MockUpgraderOS(android: true).currentOSType, UpgraderOSType.android); + expect( + MockUpgraderOS(fuchsia: true).currentOSType, UpgraderOSType.fuchsia); + expect(MockUpgraderOS(ios: true).currentOSType, UpgraderOSType.ios); + expect(MockUpgraderOS(linux: true).currentOSType, UpgraderOSType.linux); + expect(MockUpgraderOS(macos: true).currentOSType, UpgraderOSType.macos); + expect(MockUpgraderOS(web: true).currentOSType, UpgraderOSType.web); + expect( + MockUpgraderOS(windows: true).currentOSType, UpgraderOSType.windows); + }); }); } diff --git a/test/upgrader_store_controller_test.dart b/test/upgrader_store_controller_test.dart index 9bf99446..7bdae9c5 100644 --- a/test/upgrader_store_controller_test.dart +++ b/test/upgrader_store_controller_test.dart @@ -13,6 +13,7 @@ void main() { test('UpgraderAppStore returns UpgraderVersionInfo', () async { final installedVersion = Version(1, 9, 6); final state = UpgraderState( + debugLogging: true, client: MockITunesSearchClient.setupMockClient(), packageInfo: PackageInfo( appName: 'Upgrader', @@ -45,6 +46,7 @@ void main() { test('UpgraderPlayStore returns UpgraderVersionInfo', () async { final installedVersion = Version(1, 9, 6); final state = UpgraderState( + debugLogging: true, client: await MockPlayStoreSearchClient.setupMockClient(), packageInfo: PackageInfo( appName: 'Upgrader', @@ -82,6 +84,7 @@ void main() { test('UpgraderAppcastStore returns UpgraderVersionInfo', () async { final installedVersion = Version.parse('1.9.6'); final state = UpgraderState( + debugLogging: true, client: await MockPlayStoreSearchClient.setupMockClient(), packageInfo: PackageInfo( appName: 'Upgrader', diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 42b097bd..331b362c 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -78,6 +78,27 @@ void main() { }); }); + // testWidgets('test Upgrader no package info', (WidgetTester tester) async { + // await tester.runAsync(() async { + // final client = MockITunesSearchClient.setupMockClient(); + // final upgrader = Upgrader( + // upgraderOS: MockUpgraderOS(ios: true), + // client: client, + // debugLogging: true, + // ); + + // expect(tester.takeException(), null); + // await tester.pumpAndSettle(); + // try { + // expect(upgrader.appName(), 'Upgrader'); + // } catch (e) { + // expect(e, Upgrader.notInitializedExceptionMessage); + // } + + // expect(await upgrader.initialize(), isTrue); + // }); + // }); + testWidgets('test Upgrader clearSavedSettings', (WidgetTester tester) async { await Upgrader.clearSavedSettings(); }, skip: false); From b0a51cc8cabc0fca5493fa8e0a664c27fa05248a Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Wed, 14 Feb 2024 07:43:33 -0500 Subject: [PATCH 09/15] Added more unit tests. --- lib/src/upgrade_store_controller.dart | 10 +- test/mock_itunes_client.dart | 16 + test/mock_play_store_client.dart | 2 + test/play_store_test.dart | 23 + test/test_play_store_page6.txt | 1002 +++++++++++++++++++++++++ test/test_play_store_page7.txt | 57 ++ test/upgrader_test.dart | 33 + 7 files changed, 1137 insertions(+), 6 deletions(-) create mode 100644 test/test_play_store_page6.txt create mode 100644 test/test_play_store_page7.txt diff --git a/lib/src/upgrade_store_controller.dart b/lib/src/upgrade_store_controller.dart index d6caa1e4..7ba11c9c 100644 --- a/lib/src/upgrade_store_controller.dart +++ b/lib/src/upgrade_store_controller.dart @@ -47,7 +47,8 @@ class UpgraderAppStore extends UpgraderStore { appStoreVersion = Version.parse(version); } catch (e) { if (state.debugLogging) { - print('upgrader: UpgraderAppStore.appStoreVersion exception: $e'); + print( + 'upgrader: UpgraderAppStore.appStoreVersion "$version" exception: $e'); } } } @@ -103,7 +104,8 @@ class UpgraderPlayStore extends UpgraderStore { appStoreVersion = Version.parse(version); } catch (e) { if (state.debugLogging) { - print('upgrader: UpgraderPlayStore.appStoreVersion exception: $e'); + print( + 'upgrader: UpgraderPlayStore.appStoreVersion "$version" exception: $e'); } } } @@ -240,10 +242,6 @@ class UpgraderAppcastStore extends UpgraderStore { } } -class UpgraderConfiguration { - String get appStoreListingURL => throw UnimplementedError(); -} - /// A controller that provides the store details for each platform. class UpgraderStoreController { /// Creates a controller that provides the store details for each platform. diff --git a/test/mock_itunes_client.dart b/test/mock_itunes_client.dart index 874e583f..2a9800fc 100644 --- a/test/mock_itunes_client.dart +++ b/test/mock_itunes_client.dart @@ -86,6 +86,22 @@ class MockITunesSearchClient { ] }), 200); + } else if (url == + ITunesSearchAPI().lookupURLByBundleId('com.larryaasen.upgrader.4', + country: country, useCacheBuster: false)) { + return http.Response( + json.encode({ + 'results': [ + { + 'version': '7.0.a', + 'bundleId': 'com.google.Maps', + 'currency': currency, + 'releaseNotes': 'Bug fixes.', + if (description.isNotEmpty) 'description': description + } + ] + }), + 200); } if (url == ITunesSearchAPI().lookupURLByBundleId('com.google.MyApp', diff --git a/test/mock_play_store_client.dart b/test/mock_play_store_client.dart index 5f07d6dd..87791124 100644 --- a/test/mock_play_store_client.dart +++ b/test/mock_play_store_client.dart @@ -14,6 +14,8 @@ final _filenames = { 'com.testing.test3': 'test_play_store_page3.txt', 'com.testing.test4': 'test_play_store_page4.txt', 'com.testing.test5': 'test_play_store_page5.txt', + 'com.testing.test6': 'test_play_store_page6.txt', + 'com.testing.test7': 'test_play_store_page7.txt', }; // Create a MockClient using the Mock class provided by the Mockito package. diff --git a/test/play_store_test.dart b/test/play_store_test.dart index a22c9ebb..57c80d49 100644 --- a/test/play_store_test.dart +++ b/test/play_store_test.dart @@ -127,6 +127,19 @@ void main() { expect(await playStore.lookupById('com.not.a.valid.application'), isNull); }, skip: false); + test('testing lookupById with invalid version', () async { + final client = await MockPlayStoreSearchClient.setupMockClient(); + final playStore = PlayStoreSearchAPI(client: client); + + final response = await playStore.lookupById('com.testing.test7'); + expect(response, isNotNull); + expect(response, isInstanceOf()); + + expect( + playStore.releaseNotes(response!), 'Minor updates and improvements.'); + expect(playStore.version(response), isNull); + }, skip: false); + test('testing release notes', () async { final client = await MockPlayStoreSearchClient.setupMockClient(); final playStore = PlayStoreSearchAPI(client: client); @@ -169,6 +182,16 @@ void main() { expect(playStore.description(response)?.length, greaterThan(10)); }, skip: false); + test('testing invalid store version', () async { + final client = await MockPlayStoreSearchClient.setupMockClient(); + final playStore = PlayStoreSearchAPI(client: client); + + final response = await playStore.lookupById('com.testing.test6'); + expect(response, isNotNull); + expect(response, isInstanceOf()); + expect(playStore.version(response!), isNull); + }, skip: false); + /// Helper method Document resDesc(String description) { final html = diff --git a/test/test_play_store_page6.txt b/test/test_play_store_page6.txt new file mode 100644 index 00000000..f81a0987 --- /dev/null +++ b/test/test_play_store_page6.txt @@ -0,0 +1,1002 @@ + +US Debt Now - National Debt - Apps on Google Play

US Debt Now - National Debt

Contains Ads
You can share this with your family. Learn more about Family Library

The US Debt Now app displays the daily amount of the US National Debt over the past 25+ years. You can see the trending of the debt in an interactive chart with data updated each day as the Treasury publishes the new amount.

Today the US National Debt is over 28 trillion dollars and is changing every day. The actual number is $28.91T, or $28,908,820,096,413.

This is a beautiful app with a nice display that makes it quick and easy to get up-to-date details on the debt. A perfect tool for TV and radio talk show hosts, members of congress, and even the President of the United States (POTUS). Keep this tool handy to see how much money is currently in the debt.

This app does not represent the US Treasury or any government or political entity.

The daily debt data for the app is taken from the TreasuryDirect website using their "The Debt to the Penny and Who Holds It" JSON service. Browse the Treasury website https://www.treasurydirect.gov/govt/reports/pd/pd_debttothepenny.htm for more information.

[Minimum supported app version: 4.5.6]
Read more
Collapse
3.4
8 total
5
4
3
2
1
Loading…

What's New

Minor updates and improvements.
Again.
Again.
Read more
Collapse

Additional Information

Updated
November 10, 2021
Size
Varies with device
Installs
1,000+
Current Version
2.0.2c
Requires Android
6.0 and up
Content Rating
Everyone
Permissions
Offered By
Larry Aasen
©2021 GoogleSite Terms of ServicePrivacyDevelopersAbout Google|Location: United StatesLanguage: English
By purchasing this item, you are transacting with Google Payments and agreeing to the Google Payments Terms of Service and Privacy Notice.
\ No newline at end of file diff --git a/test/test_play_store_page7.txt b/test/test_play_store_page7.txt new file mode 100644 index 00000000..99f4f222 --- /dev/null +++ b/test/test_play_store_page7.txt @@ -0,0 +1,57 @@ +US Debt Now - National Debt - Apps on Google Play

US Debt Now - National Debt

Contains ads
1K+
Downloads
Content rating
All ages
Screenshot image
Screenshot image
Screenshot image
Screenshot image
Screenshot image
Screenshot image

About this app

The US Debt Now app displays the daily amount of the US National Debt over the past 25+ years. You can see the trending of the debt in an interactive chart with data updated each day as the Treasury publishes the new amount.

Today the US National Debt is over 28 trillion dollars and is changing every day. The actual number is $28.91T, or $28,908,820,096,413.

This is a beautiful app with a nice display that makes it quick and easy to get up-to-date details on the debt. A perfect tool for TV and radio talk show hosts, members of congress, and even the President of the United States (POTUS). Keep this tool handy to see how much money is currently in the debt.

This app does not represent the US Treasury or any government or political entity.

The daily debt data for the app is taken from the TreasuryDirect website using their "The Debt to the Penny and Who Holds It" JSON service. Browse the Treasury website https://www.treasurydirect.gov/govt/reports/pd/pd_debttothepenny.htm for more information.

[Minimum supported app version: 2.0]
Updated on
Feb 21, 2022

Data safety

Developers can show information here about how their app collects and uses your data. Learn more about data safety
No information available

What's new

Minor updates and improvements.
\ No newline at end of file diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 331b362c..5982fb8d 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -169,6 +169,7 @@ void main() { await upgrader.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(upgrader.isUpdateAvailable(), true); + expect(upgrader.currentAppStoreVersion, '7.0.0'); upgrader.installPackageInfo( packageInfo: PackageInfo( @@ -179,6 +180,17 @@ void main() { await upgrader.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(upgrader.isUpdateAvailable(), false); + + upgrader.installPackageInfo( + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.larryaasen.upgrader.4', + version: '1.9.9', + buildNumber: '400')); + + await upgrader.didChangeAppLifecycleState(AppLifecycleState.resumed); + expect(upgrader.currentAppStoreVersion, isNull); + expect(upgrader.isUpdateAvailable(), false); }); }); @@ -625,6 +637,27 @@ void main() { expect(upgrader.state.versionInfo?.minAppVersion.toString(), '4.5.6'); }, skip: false); + testWidgets('test upgrader store version android', + (WidgetTester tester) async { + final client = await MockPlayStoreSearchClient.setupMockClient(); + final upgrader = Upgrader( + upgraderOS: MockUpgraderOS(android: true), + client: client, + debugLogging: true); + + upgrader.installPackageInfo( + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.testing.test7', + version: '2.9.9', + buildNumber: '400')); + upgrader.initialize().then((value) {}); + await tester.pumpAndSettle(); + + expect(upgrader.belowMinAppVersion(), isFalse); + expect(upgrader.state.versionInfo?.appStoreVersion, isNull); + }, skip: false); + testWidgets('test upgrader minAppVersion description ios', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient( From 622ba2394ff723126c436dbcfd168680b7dc522e Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Thu, 15 Feb 2024 08:59:57 -0500 Subject: [PATCH 10/15] Bumped version to 10.0.0-alpha.1. --- CHANGELOG.md | 4 ++-- example/lib/generated_plugin_registrant.dart | 19 ------------------- example/lib/main_appcast.dart | 5 +---- lib/src/upgrader.dart | 10 ++++++++-- pubspec.yaml | 2 +- 5 files changed, 12 insertions(+), 28 deletions(-) delete mode 100644 example/lib/generated_plugin_registrant.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index e6f2bff1..922aeb6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ -## Next +## 10.0.0-alpha.1 - Implemented [UpgraderState] that is used internally to replace evaluation ready. -- Removed Appcast configuration so that an Appcast [UpgraderStore] can be used. +- BREAKING: Removed Appcast configuration so that an Appcast [UpgraderStore] can be used. ## 9.0.0 diff --git a/example/lib/generated_plugin_registrant.dart b/example/lib/generated_plugin_registrant.dart deleted file mode 100644 index 6a1a4a29..00000000 --- a/example/lib/generated_plugin_registrant.dart +++ /dev/null @@ -1,19 +0,0 @@ -// -// Generated file. Do not edit. -// - -// ignore_for_file: directives_ordering -// ignore_for_file: lines_longer_than_80_chars -// ignore_for_file: depend_on_referenced_packages - -import 'package:shared_preferences_web/shared_preferences_web.dart'; -import 'package:url_launcher_web/url_launcher_web.dart'; - -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; - -// ignore: public_member_api_docs -void registerPlugins(Registrar registrar) { - SharedPreferencesPlugin.registerWith(registrar); - UrlLauncherPlugin.registerWith(registrar); - registrar.registerMessageHandler(); -} diff --git a/example/lib/main_appcast.dart b/example/lib/main_appcast.dart index 7cc4dc09..bcba1258 100644 --- a/example/lib/main_appcast.dart +++ b/example/lib/main_appcast.dart @@ -11,10 +11,6 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, setup the Appcast. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. runApp(MyApp()); } @@ -26,6 +22,7 @@ class MyApp extends StatelessWidget { final upgrader = Upgrader( storeController: UpgraderStoreController( onAndroid: () => UpgraderAppcastStore(appcastURL: appcastURL), + oniOS: () => UpgraderAppcastStore(appcastURL: appcastURL), ), ); diff --git a/lib/src/upgrader.dart b/lib/src/upgrader.dart index 9687fb5d..5ed0585d 100644 --- a/lib/src/upgrader.dart +++ b/lib/src/upgrader.dart @@ -133,8 +133,14 @@ class Upgrader with WidgetsBindingObserver { if (state.debugLogging) print('upgrader: ${state.upgraderOS}'); if (state.packageInfo == null) { - final packageInfo = await PackageInfo.fromPlatform(); - updateState(state.copyWith(packageInfo: packageInfo)); + try { + final packageInfo = await PackageInfo.fromPlatform(); + updateState(state.copyWith(packageInfo: packageInfo)); + } catch (e) { + if (state.debugLogging) { + print('upgrader: PackageInfo exception: $e'); + } + } } final packageInfo = state.packageInfo; diff --git a/pubspec.yaml b/pubspec.yaml index f1325ed2..85c33c6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: upgrader description: Flutter package for prompting users to upgrade when there is a newer version of the app in the store. -version: 9.0.0 +version: 10.0.0-alpha.1 homepage: https://github.com/larryaasen/upgrader environment: From 6b1b0521c372ecc39909d85b945222a55379ff98 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sun, 18 Feb 2024 20:35:15 -0500 Subject: [PATCH 11/15] Updated some documentation and unit tests. --- .github/workflows/main.yml | 2 +- CHANGELOG.md | 10 +++++ README.md | 90 +++++++++++++++++++++++++------------- lib/src/upgrade_state.dart | 3 +- lib/src/upgrader.dart | 6 +-- test/appcast_test.dart | 4 +- test/testappcast.xml | 2 + 7 files changed, 80 insertions(+), 37 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a05dd070..1271f588 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: runs-on: macos-latest strategy: matrix: - flutter-version: ['3.13.1'] + flutter-version: ['3.13.1', '3.19.0'] steps: - uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 922aeb6d..3ad20e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 10.0.0 next + +This major update changes the structure of how the internal state is maintained and how access to app stores is provided. The API has not changed for the standard use cases. However, the way in which Appcast is used has changed slightly. + +This update also makes it easier to extend upgrader to support more app stores without changing upgrader. This will come into play when used on Linux and Windows, or when supporting alternate app stores on Android. + +### Changes +- Implemented [UpgraderState] that is used internally to replace evaluation ready. +- BREAKING: Removed Appcast configuration so that an Appcast [UpgraderStore] can be used. + ## 10.0.0-alpha.1 - Implemented [UpgraderState] that is used internally to replace evaluation ready. diff --git a/README.md b/README.md index fc8d7035..478c1289 100644 --- a/README.md +++ b/README.md @@ -146,21 +146,45 @@ Here are the custom parameters for `UpgradeCard`: The `Upgrader` class can be customized by setting parameters in the constructor, and passing it -* appcast: Provide an Appcast that can be replaced for mock testing, defaults to ```null``` -* appcastConfig: the appcast configuration, defaults to ```null``` -* client: an HTTP Client that can be replaced for mock testing, defaults to ```null``` +* client: an HTTP Client that can be replaced for mock testing, defaults to `http.Client()`. * countryCode: the country code that will override the system locale, which defaults to ```null``` -* languageCode: the language code that will override the system locale, which defaults to ```null``` * debugDisplayAlways: always force the upgrade to be available, defaults to ```false``` * debugDisplayOnce: display the upgrade at least once, defaults to ```false``` * debugLogging: display logging statements, which defaults to ```false``` * durationUntilAlertAgain: duration until alerting user again, which defaults to ```3 days``` +* languageCode: the language code that will override the system locale, which defaults to ```null``` * messages: optional localized messages used for display in `upgrader` -* minAppVersion: the minimum app version supported by this app. Earlier versions of this app will be forced to update to the current version. It should be a valid version string like this: ```2.0.13```. Defaults to ```null```. -* upgraderOS: Provides information on which OS this code is running on, defaults to ```null``` +* minAppVersion: the minimum app version supported by this app. Earlier versions of this app will be forced to update to the current version. It should be a valid version string like this: ```2.0.13```. Overrides any minimum app version from UpgraderStore. Defaults to ```null```. +* storeController: a controller that provides the store details for each platform, defaults to `UpgraderStoreController()`. +* upgraderDevice: an abstraction of the device_info details which is used for the OS version, defaults to `UpgraderDevice()`. +* upgraderOS: information on which OS this code is running on, defaults to `UpgraderOS()`. * willDisplayUpgrade: called when ```upgrader``` determines that an upgrade may or may not be displayed, defaults to ```null``` +The `UpgraderStoreController` class is a controller that provides the store details +for each platform. +* onAndroid: defaults to `UpgraderPlayStore()` that extends `UpgraderStore`. +* onFuchsia: defaults to `UpgraderAppStore()` that extends `UpgraderStore`. +* oniOS: defaults to `null`. +* onLinux: defaults to `null`. +* onMacOS: defaults to `null`. +* onWeb: defaults to `null`. +* onWindows: defaults to `null`. + +To change the `UpgraderStore` for a platform, replace the platform with a +different store. Here is an example of using an Appcast on iOS. +``` +final upgrader = Upgrader( + storeController: UpgraderStoreController( + onAndroid: () => UpgraderPlayStore(), + oniOS: () => UpgraderAppcastStore(appcastURL: appcastURL), + ), +); +``` + +You can even subclass `UpgraderStore` or an existing store class like +`UpgraderPlayStore` to provide your own customization. + ## Minimum App Version The `upgrader` package can enforce a minimum app version simply by adding a version number to the description field in the app stores. @@ -204,7 +228,7 @@ a navigatorKey to the ```UpgradeAlert``` widget so that the correct route context is used. Below is part of the code you will need for this. Also, checkout the [example/lib/main-gorouter.dart](example/lib/main-gorouter.dart) example for a more complete example. -``` +```dart @override Widget build(BuildContext context) { return MaterialApp.router( @@ -260,22 +284,22 @@ There is an [appcast](#appcast) that can be used to remotely configure the latest app version. See [appcast](#appcast) below for more details. ## Appcast - -The class [Appcast](lib/src/appcast.dart), in this Flutter package, is used by the `upgrader` widgets -to download app details from an appcast, -based on the [Sparkle](https://sparkle-project.org/) framework by Andy Matuschak. -You can read the Sparkle documentation here: -https://sparkle-project.org/documentation/publishing/. +The `upgrader` package supports Appcast as an `UpgraderStore`. An appcast is an RSS feed with one channel that has a collection of items that each describe one app version. The appcast will describe each app version and will provide the latest app version to `upgrader` that indicates when an upgrade should be recommended. +Appcast is based on the [Sparkle](https://sparkle-project.org/) framework by Andy Matuschak. +You can read the Sparkle documentation here: +https://sparkle-project.org/documentation/publishing/. + The appcast must be hosted on a server that can be reached by everyone from the app. The appcast XML file can be autogenerated during the release process, or just manually updated after a release is available on the app store. -The Appcast class can be used stand alone or as part of `upgrader`. +The class [UpgraderAppcastStore](lib/src/upgrade_store_controller.dart), in this +Flutter package, is used by `upgrader` to download app details from an appcast. ### Appcast Example This is an Appcast example for Android. @@ -283,8 +307,10 @@ This is an Appcast example for Android. static const appcastURL = 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; final upgrader = Upgrader( - appcastConfig: - AppcastConfiguration(url: appcastURL, supportedOS: ['android'])); + storeController: UpgraderStoreController( + onAndroid: () => UpgraderAppcastStore(appcastURL: appcastURL), + ), +); @override Widget build(BuildContext context) { @@ -419,7 +445,7 @@ class MySpanishMessages extends UpgraderMessages { } } -UpgradeAlert(Upgrader(messages: MySpanishMessages())); +UpgradeAlert(upgrader: Upgrader(messages: MySpanishMessages())); ``` You can even force the `upgrader` package to use a specific language, instead of the @@ -427,7 +453,7 @@ system language on the device. Just pass the language code to an instance of UpgraderMessages when displaying the alert or card. Here is an example: ```dart -UpgradeAlert(Upgrader(messages: UpgraderMessages(code: 'es'))); +UpgradeAlert(upgrader: Upgrader(messages: UpgraderMessages(code: 'es'))); ``` ## Semantic Versioning @@ -501,29 +527,33 @@ which can be enabled by setting `debugLogging` to `true`. It should look something like this: ``` -flutter: upgrader: languageCode: en -flutter: upgrader: build UpgradeAlert -flutter: upgrader: default operatingSystem: ios 11.4 -flutter: upgrader: operatingSystem: ios -flutter: upgrader: platform: TargetPlatform.iOS -flutter: upgrader: package info packageName: com.google.Maps -flutter: upgrader: package info appName: Upgrader -flutter: upgrader: package info version: 1.0.0 +flutter: upgrader: operatingSystem: ios, version: Version 17.0.1 (Build 21A342) +flutter: upgrader: packageInfo packageName: com.google.Maps +flutter: upgrader: packageInfo appName: Upgrader +flutter: upgrader: packageInfo version: 1.0.0 +flutter: upgrader: current locale: en_US flutter: upgrader: countryCode: US +flutter: upgrader: languageCode: en +flutter: upgrader: download: https://itunes.apple.com/lookup?bundleId=com.google.Maps&country=US&_cb=1708305624824631 +flutter: upgrader: response statusCode: 200 +flutter: upgrader: UpgraderAppStore: version info: appStoreListingURL: https://apps.apple.com/us/app/google-maps/id585027354?uo=4, appStoreVersion: 6.102.3, installedVersion: 1.0.0, isCriticalUpdate: null, minAppVersion: null, releaseNotes: Thanks for using Google Maps! This release brings bug fixes that improve our product to help you discover new places and navigate to them. +flutter: upgrader: need to evaluate version flutter: upgrader: blocked: false flutter: upgrader: debugDisplayAlways: false flutter: upgrader: debugDisplayOnce: false flutter: upgrader: hasAlerted: false -flutter: upgrader: appStoreVersion: 5.81 flutter: upgrader: installedVersion: 1.0.0 flutter: upgrader: minAppVersion: null flutter: upgrader: isUpdateAvailable: true flutter: upgrader: shouldDisplayUpgrade: true flutter: upgrader: shouldDisplayReleaseNotes: true -flutter: upgrader: showDialog title: Update App? -flutter: upgrader: showDialog message: A new version of Upgrader is available! Version 5.81 is now available-you have 1.0.0. -flutter: upgrader: showDialog releaseNotes: Thanks for using Google Maps! This release brings bug fixes that improve our product to help you discover new places and navigate to them. +flutter: upgrader: current locale: en_US +flutter: upgrader: languageCode: en +flutter: upgrader: showTheDialog title: Update App? +flutter: upgrader: showTheDialog message: A new version of Upgrader is available! Version 6.102.3 is now available-you have 1.0.0. +flutter: upgrader: showTheDialog releaseNotes: Thanks for using Google Maps! This release brings bug fixes that improve our product to help you discover new places and navigate to them. ``` + Also, please include the upgrader version number from the pubspec.lock file, which should look something like this: ``` upgrader: diff --git a/lib/src/upgrade_state.dart b/lib/src/upgrade_state.dart index f8a1b1ff..80328749 100644 --- a/lib/src/upgrade_state.dart +++ b/lib/src/upgrade_state.dart @@ -54,7 +54,8 @@ class UpgraderState { final UpgraderMessages? messages; /// The minimum app version supported by this app. Earlier versions of this app - /// will be forced to update to the current version. Optional. + /// will be forced to update to the current version. Overrides any minimum + /// app version from UpgraderStore. Optional. final Version? minAppVersion; /// The app package metadata information. diff --git a/lib/src/upgrader.dart b/lib/src/upgrader.dart index 5ed0585d..f564bc7e 100644 --- a/lib/src/upgrader.dart +++ b/lib/src/upgrader.dart @@ -45,19 +45,19 @@ class Upgrader with WidgetsBindingObserver { /// Creates an uprade controller that maintains a [state] that is used to /// trigger an alert or other UI to evaluate upgrading criteria. Upgrader({ + http.Client? client, + String? countryCode, bool debugDisplayAlways = false, bool debugDisplayOnce = false, bool debugLogging = false, Duration durationUntilAlertAgain = const Duration(days: 3), - this.willDisplayUpgrade, - http.Client? client, - String? countryCode, String? languageCode, UpgraderMessages? messages, String? minAppVersion, UpgraderStoreController? storeController, UpgraderDevice? upgraderDevice, UpgraderOS? upgraderOS, + this.willDisplayUpgrade, }) : _state = UpgraderState( client: client ?? http.Client(), countryCodeOverride: countryCode, diff --git a/test/appcast_test.dart b/test/appcast_test.dart index dd1dfe0f..9626b629 100644 --- a/test/appcast_test.dart +++ b/test/appcast_test.dart @@ -212,7 +212,7 @@ void validateItems(List items, Appcast appcast) { expect(items[0].osString, isNull); expect(items[1].title, equals('Version 3.0')); - expect(items[1].itemDescription, equals(null)); + expect(items[1].itemDescription, equals('Minor updates and improvements.')); expect(items[1].dateString, equals(null)); expect( items[1].fileURL, equals('http://localhost:1337/Sparkle_Test_App.zip')); @@ -226,7 +226,7 @@ void validateItems(List items, Appcast appcast) { expect(items[1].osString, equals('android')); expect(items[2].title, equals('Version 4.0')); - expect(items[2].itemDescription, equals(null)); + expect(items[2].itemDescription, equals('Minor updates and improvements.')); expect(items[2].dateString, 'Sat, 26 Jul 2014 15:20:13 +0000'); expect( items[2].fileURL, equals('http://localhost:1337/Sparkle_Test_App.zip')); diff --git a/test/testappcast.xml b/test/testappcast.xml index e7a021d7..25cdcbf9 100644 --- a/test/testappcast.xml +++ b/test/testappcast.xml @@ -12,6 +12,7 @@ Version 3.0 + Minor updates and improvements. Version 4.0 + Minor updates and improvements. Sat, 26 Jul 2014 15:20:13 +0000 17.0.0 From 7a9deea26d8f28f85acaf62f638d8b4d288a97df Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Tue, 20 Feb 2024 07:45:57 -0500 Subject: [PATCH 12/15] Bumped version to 10.0.0-alpha.2 --- CHANGELOG.md | 4 +++- pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad20e9c..930cb849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ -## 10.0.0 next +## 10.0.0-alpha.2 + +(README file and documentation updates) This major update changes the structure of how the internal state is maintained and how access to app stores is provided. The API has not changed for the standard use cases. However, the way in which Appcast is used has changed slightly. diff --git a/pubspec.yaml b/pubspec.yaml index 85c33c6c..7b1a7af7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: upgrader description: Flutter package for prompting users to upgrade when there is a newer version of the app in the store. -version: 10.0.0-alpha.1 +version: 10.0.0-alpha.2 homepage: https://github.com/larryaasen/upgrader environment: From 4cbc70d513e1d57f8a8d2e5ad1feca282de60d49 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sat, 24 Feb 2024 10:41:54 -0500 Subject: [PATCH 13/15] Fixed deprecation warning for WillPopScope and replaced it with PopScope, which required the minimum Flutter SDK version to be moved up to 3.16.0 in this package. Renamed parameter canDismissDialog to barrierDismissible in `UpgradeAlert`. --- CHANGELOG.md | 19 ++++++- README.md | 4 +- example/android/app/build.gradle | 2 +- example/lib/main.dart | 1 + example/lib/main_custom_alert.dart | 2 +- .../Flutter/GeneratedPluginRegistrant.swift | 2 +- lib/src/upgrade_alert.dart | 55 ++++++++++--------- lib/src/upgrader.dart | 9 +-- pubspec.yaml | 4 +- 9 files changed, 58 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 930cb849..8867cc09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## 10.0.0-alpha.3 + +- Fixed deprecation warning for WillPopScope and replaced it with PopScope, which required the minimum Flutter SDK version to be moved up to 3.16.0 in this package. +- Renamed parameter canDismissDialog to barrierDismissible in `UpgradeAlert`. + +### 10.0.0 + +This major update changes the structure of how the internal state is maintained and how access to app stores is provided. The API has not changed for the standard use cases. However, the way in which Appcast is used has changed slightly. + +This update also makes it easier to extend upgrader to support more app stores without changing upgrader. This will come into play when used on Linux and Windows, or when supporting alternate app stores on Android. + +### Changes in 10.0.0 +- Implemented [UpgraderState] that is used internally to replace evaluation ready. +- BREAKING: Removed Appcast configuration so that an Appcast [UpgraderStore] can be used. +- Fixed deprecation warning for WillPopScope and replaced it with PopScope, which required the minimum Flutter SDK version to be moved up to 3.16.0 in this package. +- Renamed parameter canDismissDialog to barrierDismissible in `UpgradeAlert`. + ## 10.0.0-alpha.2 (README file and documentation updates) @@ -6,7 +23,7 @@ This major update changes the structure of how the internal state is maintained This update also makes it easier to extend upgrader to support more app stores without changing upgrader. This will come into play when used on Linux and Windows, or when supporting alternate app stores on Android. -### Changes +### Changes in 10.0.0 - Implemented [UpgraderState] that is used internally to replace evaluation ready. - BREAKING: Removed Appcast configuration so that an Appcast [UpgraderStore] can be used. diff --git a/README.md b/README.md index 478c1289..71a143fe 100644 --- a/README.md +++ b/README.md @@ -121,13 +121,13 @@ The card can be customized by changing the `CardTheme` on the `MaterialApp`, or Here are the custom parameters for `UpgradeAlert`: -* canDismissDialog: can alert dialog be dismissed on tap outside of the alert dialog, which defaults to ```false``` (not used by UpgradeCard) +* barrierDismissible: used to indicate whether tapping on the barrier will dismiss the dialog, which defaults to ```false``` * cupertinoButtonTextStyle: the text style for the cupertino dialog buttons, which defaults to ```null``` * dialogStyle: the upgrade dialog style, either ```material``` or ```cupertino```, defaults to ```material```, used only by UpgradeAlert, works on Android and iOS. * onIgnore: called when the ignore button is tapped, defaults to ```null``` * onLater: called when the later button is tapped, defaults to ```null``` * onUpdate: called when the update button is tapped, defaults to ```null``` -* shouldPopScope: called when the back button is tapped, defaults to ```null``` +* shouldPopScope: called to determine if the dialog blocks the current route from being popped, which defaults to ```null``` * showIgnore: hide or show Ignore button, which defaults to ```true``` * showLater: hide or show Later button, which defaults to ```true``` * showReleaseNotes: hide or show release notes, which defaults to ```true``` diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 0d409e07..05fec29d 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/example/lib/main.dart b/example/lib/main.dart index 828d9954..b76e0b0f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -27,6 +27,7 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Upgrader Example', home: UpgradeAlert( + upgrader: Upgrader(debugLogging: true), child: Scaffold( appBar: AppBar(title: const Text('Upgrader Example')), body: const Center(child: Text('Checking...')), diff --git a/example/lib/main_custom_alert.dart b/example/lib/main_custom_alert.dart index d3782aef..8c69d4ea 100644 --- a/example/lib/main_custom_alert.dart +++ b/example/lib/main_custom_alert.dart @@ -61,7 +61,7 @@ class MyUpgradeAlertState extends UpgradeAlertState { required String? title, required String message, required String? releaseNotes, - required bool canDismissDialog, + required bool barrierDismissible, required UpgraderMessages messages, }) { showDialog( diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 92513f68..a84f33af 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,7 +12,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/lib/src/upgrade_alert.dart b/lib/src/upgrade_alert.dart index 3ed857db..2dce347b 100644 --- a/lib/src/upgrade_alert.dart +++ b/lib/src/upgrade_alert.dart @@ -20,7 +20,7 @@ class UpgradeAlert extends StatefulWidget { UpgradeAlert({ super.key, Upgrader? upgrader, - this.canDismissDialog = false, + this.barrierDismissible = false, this.dialogStyle = UpgradeDialogStyle.material, this.onIgnore, this.onLater, @@ -38,8 +38,9 @@ class UpgradeAlert extends StatefulWidget { /// The upgraders used to configure the upgrade dialog. final Upgrader upgrader; - /// Can alert dialog be dismissed on tap outside of the alert dialog. Not used by [UpgradeCard]. (default: false) - final bool canDismissDialog; + /// The `barrierDismissible` argument is used to indicate whether tapping on the + /// barrier will dismiss the dialog. (default: false) + final bool barrierDismissible; /// The upgrade dialog style. Used only on UpgradeAlert. (default: material) final UpgradeDialogStyle dialogStyle; @@ -55,9 +56,7 @@ class UpgradeAlert extends StatefulWidget { /// Return false when the default behavior should not execute. final BoolCallback? onUpdate; - /// Called when the user taps outside of the dialog and [canDismissDialog] - /// is false. Also called when the back button is pressed. Return true for - /// the screen to be popped. + /// Called to determine if the dialog blocks the current route from being popped. final BoolCallback? shouldPopScope; /// Hide or show Ignore button on dialog (default: true) @@ -132,7 +131,6 @@ class UpgradeAlertState extends State { } /// Will show the alert dialog when it should be dispalyed. - /// Only called by [UpgradeAlert] and not used by [UpgradeCard]. void checkVersion({required BuildContext context}) { final shouldDisplay = widget.upgrader.shouldDisplayUpgrade(); if (widget.upgrader.state.debugLogging) { @@ -150,7 +148,7 @@ class UpgradeAlertState extends State { message: widget.upgrader.body(appMessages), releaseNotes: shouldDisplayReleaseNotes ? widget.upgrader.releaseNotes : null, - canDismissDialog: widget.canDismissDialog, + barrierDismissible: widget.barrierDismissible, messages: appMessages, ); }); @@ -220,7 +218,7 @@ class UpgradeAlertState extends State { required String? title, required String message, required String? releaseNotes, - required bool canDismissDialog, + required bool barrierDismissible, required UpgraderMessages messages, }) { if (widget.upgrader.state.debugLogging) { @@ -233,30 +231,35 @@ class UpgradeAlertState extends State { widget.upgrader.saveLastAlerted(); showDialog( - barrierDismissible: canDismissDialog, + barrierDismissible: barrierDismissible, context: context, builder: (BuildContext context) { - return WillPopScope( - onWillPop: () async => onWillPop(), - child: alertDialog( - key, - title ?? '', - message, - releaseNotes, - context, - widget.dialogStyle == UpgradeDialogStyle.cupertino, - messages, - )); + return PopScope( + canPop: onCanPop(), + onPopInvoked: (didPop) { + if (widget.upgrader.state.debugLogging) { + print('upgrader: showTheDialog onPopInvoked: $didPop'); + } + }, + child: alertDialog( + key, + title ?? '', + message, + releaseNotes, + context, + widget.dialogStyle == UpgradeDialogStyle.cupertino, + messages, + ), + ); }, ); } - /// Called when the user taps outside of the dialog and [canDismissDialog] - /// is false. Also called when the back button is pressed. Return true for - /// the screen to be popped. Defaults to false. - bool onWillPop() { + /// Determines if the dialog blocks the current route from being popped. + /// Will return the result from [shouldPopScope] if it is not null, otherwise it will return false. + bool onCanPop() { if (widget.upgrader.state.debugLogging) { - print('upgrader: onWillPop called'); + print('upgrader: onCanPop called'); } if (widget.shouldPopScope != null) { final should = widget.shouldPopScope!(); diff --git a/lib/src/upgrader.dart b/lib/src/upgrader.dart index f564bc7e..2f925bd7 100644 --- a/lib/src/upgrader.dart +++ b/lib/src/upgrader.dart @@ -211,6 +211,9 @@ class Upgrader with WidgetsBindingObserver { // Determine the store to be used for this app. final store = storeController.getUpgraderStore(state.upgraderOS); if (store == null) { + if (state.debugLogging) { + print('upgrader: updateVersionInfo found no store controller'); + } updateState(state.copyWithNull(versionInfo: null)); return null; } @@ -255,12 +258,6 @@ class Upgrader with WidgetsBindingObserver { return versionInfo; } - /// Android info is fetched by parsing the html of the app store page. - Future getAndroidStoreVersion( - {String? country, String? language}) async { - return true; - } - bool verifyInit() { if (!_initCalled) { throw (notInitializedExceptionMessage); diff --git a/pubspec.yaml b/pubspec.yaml index 7b1a7af7..b28e45f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: upgrader description: Flutter package for prompting users to upgrade when there is a newer version of the app in the store. -version: 10.0.0-alpha.2 +version: 10.0.0-alpha.3 homepage: https://github.com/larryaasen/upgrader environment: sdk: '>=3.1.0 <4.0.0' - flutter: ">=3.13.1" + flutter: ">=3.16.0" dependencies: flutter: From 1fdd6eb5b1ba819dd3b629d6f0b0b80282145daa Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sat, 24 Feb 2024 10:49:37 -0500 Subject: [PATCH 14/15] Fixed GitHub actions --- .github/workflows/main.yml | 2 +- .github/workflows/platforms.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1271f588..8ddb2524 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: runs-on: macos-latest strategy: matrix: - flutter-version: ['3.13.1', '3.19.0'] + flutter-version: ['3.16.0', '3.19.1'] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index fa2ff82e..cf56827c 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: runs-on-name: ['windows-latest'] - flutter-version: ['3.3.12'] + flutter-version: ['3.16.0'] runs-on: ${{ matrix.runs-on-name }} From c69c519167446095cec8682c7cdf19f8cefff06c Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Mon, 18 Mar 2024 10:07:42 -0400 Subject: [PATCH 15/15] Changed version to 10.0.0 for release. Added unit tests. --- CHANGELOG.md | 6 ++++ pubspec.yaml | 2 +- test/mock_play_store_client.dart | 1 + test/play_store_test.dart | 23 +++++++++++++ test/test_play_store_page8.txt | 57 ++++++++++++++++++++++++++++++++ test/upgrader_test.dart | 23 ++++++++++++- 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 test/test_play_store_page8.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 8867cc09..ebec87bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 10.0.0 + +This major update changes the structure of how the internal state is maintained and how access to app stores is provided. The API has not changed for the standard use cases. However, the way in which Appcast is used has changed slightly. + +This update also makes it easier to extend upgrader to support more app stores without changing upgrader. This will come into play when used on Linux and Windows, or when supporting alternate app stores on Android. + ## 10.0.0-alpha.3 - Fixed deprecation warning for WillPopScope and replaced it with PopScope, which required the minimum Flutter SDK version to be moved up to 3.16.0 in this package. diff --git a/pubspec.yaml b/pubspec.yaml index b28e45f8..d5d6a4b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: upgrader description: Flutter package for prompting users to upgrade when there is a newer version of the app in the store. -version: 10.0.0-alpha.3 +version: 10.0.0 homepage: https://github.com/larryaasen/upgrader environment: diff --git a/test/mock_play_store_client.dart b/test/mock_play_store_client.dart index 87791124..26ca99b2 100644 --- a/test/mock_play_store_client.dart +++ b/test/mock_play_store_client.dart @@ -16,6 +16,7 @@ final _filenames = { 'com.testing.test5': 'test_play_store_page5.txt', 'com.testing.test6': 'test_play_store_page6.txt', 'com.testing.test7': 'test_play_store_page7.txt', + 'com.testing.test8': 'test_play_store_page8.txt', }; // Create a MockClient using the Mock class provided by the Mockito package. diff --git a/test/play_store_test.dart b/test/play_store_test.dart index 57c80d49..cc74fed5 100644 --- a/test/play_store_test.dart +++ b/test/play_store_test.dart @@ -127,6 +127,29 @@ void main() { expect(await playStore.lookupById('com.not.a.valid.application'), isNull); }, skip: false); + test( + 'testing lookupById with redesignedVersion title with special characters', + () async { + final client = await MockPlayStoreSearchClient.setupMockClient(); + final playStore = PlayStoreSearchAPI(client: client); + + final response = await playStore.lookupById('com.testing.test8'); + expect(response, isNotNull); + expect(response, isInstanceOf()); + + expect( + playStore.releaseNotes(response!), 'Minor updates and improvements.'); + expect(playStore.version(response), '2.3.0'); + expect(playStore.description(response)?.length, greaterThan(10)); + expect( + pmav(response, + tagRES: + r'\[\Minimum supported app version\:[\s]*(?[^\s]+)[\s]*\]'), + '2.0.0'); + + expect(await playStore.lookupById('com.not.a.valid.application'), isNull); + }, skip: false); + test('testing lookupById with invalid version', () async { final client = await MockPlayStoreSearchClient.setupMockClient(); final playStore = PlayStoreSearchAPI(client: client); diff --git a/test/test_play_store_page8.txt b/test/test_play_store_page8.txt new file mode 100644 index 00000000..287a3cc2 --- /dev/null +++ b/test/test_play_store_page8.txt @@ -0,0 +1,57 @@ +US Debt Now - “National’s” Debt - Apps on Google Play

US Debt Now - “National’s” Debt

Contains ads
1K+
Downloads
Content rating
All ages
Screenshot image
Screenshot image
Screenshot image
Screenshot image
Screenshot image
Screenshot image

About this app

The US Debt Now app displays the daily amount of the US National Debt over the past 25+ years. You can see the trending of the debt in an interactive chart with data updated each day as the Treasury publishes the new amount.

Today the US National Debt is over 28 trillion dollars and is changing every day. The actual number is $28.91T, or $28,908,820,096,413.

This is a beautiful app with a nice display that makes it quick and easy to get up-to-date details on the debt. A perfect tool for TV and radio talk show hosts, members of congress, and even the President of the United States (POTUS). Keep this tool handy to see how much money is currently in the debt.

This app does not represent the US Treasury or any government or political entity.

The daily debt data for the app is taken from the TreasuryDirect website using their "The Debt to the Penny and Who Holds It" JSON service. Browse the Treasury website https://www.treasurydirect.gov/govt/reports/pd/pd_debttothepenny.htm for more information.

[Minimum supported app version: 2.0]
Updated on
Feb 21, 2022

Data safety

Developers can show information here about how their app collects and uses your data. Learn more about data safety
No information available

What's new

Minor updates and improvements.
\ No newline at end of file diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 5982fb8d..91b581f4 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -103,9 +103,30 @@ void main() { await Upgrader.clearSavedSettings(); }, skip: false); + testWidgets('test Upgrader stand alone', (WidgetTester tester) async { + final client = MockITunesSearchClient.setupMockClient(); + final upgrader = Upgrader( + upgraderOS: MockUpgraderOS(ios: true), + client: client, + debugLogging: true); + + upgrader.installPackageInfo( + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.larryaasen.upgrader', + version: '0.9.9', + buildNumber: '400')); + + upgrader.initialize().then((value) {}); + + await tester.pumpAndSettle(); + + expect(upgrader.isUpdateAvailable(), true); + expect(upgrader.currentAppStoreVersion, '5.6.0'); + }); + testWidgets('test Upgrader class', (WidgetTester tester) async { await tester.runAsync(() async { - // test code here final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader( upgraderOS: MockUpgraderOS(ios: true),