From 8e626e53d163a473322f6da61d03389acc445cde Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Fri, 19 Jan 2024 08:16:49 -0500 Subject: [PATCH] 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; };