diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a05dd070..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'] + 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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 31e1cc7f..ebec87bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,43 @@ +## 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. +- 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) + +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. + +## 10.0.0-alpha.1 + +- Implemented [UpgraderState] that is used internally to replace evaluation ready. +- BREAKING: Removed Appcast configuration so that an Appcast [UpgraderStore] can be used. + ## 9.0.0 ### BREAKING CHANGES diff --git a/README.md b/README.md index 6c6be291..71a143fe 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) { @@ -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``` @@ -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,14 +453,17 @@ 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 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 @@ -498,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/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/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/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-alert-theme.dart b/example/lib/main-alert-theme.dart deleted file mode 100644 index 54587a3f..00000000 --- a/example/lib/main-alert-theme.dart +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2023 Larry Aasen. All rights reserved. - -import 'package:flutter/material.dart'; -import 'package:upgrader/upgrader.dart'; - -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - - // 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({super.key}); - - final dark = ThemeData.dark(useMaterial3: true); - - final light = ThemeData( - dialogTheme: DialogTheme( - titleTextStyle: TextStyle(color: Colors.red, fontSize: 48), - contentTextStyle: TextStyle(color: Colors.green, fontSize: 18), - ), - // Change the text buttons. - textButtonTheme: const TextButtonThemeData( - style: ButtonStyle( - // Change the color of the text buttons. - foregroundColor: MaterialStatePropertyAll(Colors.orange), - ), - ), - ); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Upgrader Example', - home: UpgradeAlert( - child: Scaffold( - appBar: AppBar(title: Text('Upgrader Alert Theme Example')), - body: Center(child: Text('Checking...')), - )), - theme: light, - darkTheme: dark, - ); - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart index abb11ecf..b76e0b0f 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'; @@ -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,12 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Upgrader Example', home: UpgradeAlert( - child: Scaffold( - appBar: AppBar(title: Text('Upgrader Example')), - body: Center(child: Text('Checking...')), - )), + 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_alert_theme.dart b/example/lib/main_alert_theme.dart index 5137003e..2e54af7a 100644 --- a/example/lib/main_alert_theme.dart +++ b/example/lib/main_alert_theme.dart @@ -9,27 +9,28 @@ 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 { - MyApp({super.key}); + const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Upgrader Example', home: MyUpgradeAlert( - child: Scaffold( - appBar: AppBar(title: Text('Upgrader Alert Theme Example')), - body: Center(child: Text('Checking...')), - )), + child: Scaffold( + appBar: AppBar(title: const Text('Upgrader Alert Theme Example')), + body: const Center(child: Text('Checking...')), + ), + ), ); } } 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. @@ -49,8 +50,16 @@ class MyUpgradeAlertState extends UpgradeAlertState { UpgraderMessages messages) { return Theme( data: ThemeData( - dialogTheme: DialogTheme( - titleTextStyle: TextStyle(color: Colors.red, fontSize: 48.0)), + dialogTheme: const DialogTheme( + titleTextStyle: TextStyle(color: Colors.red, fontSize: 48), + contentTextStyle: TextStyle(color: Colors.green, fontSize: 18), + ), + textButtonTheme: const TextButtonThemeData( + style: ButtonStyle( + // Change the color of the text buttons. + foregroundColor: MaterialStatePropertyAll(Colors.orange), + ), + ), ), child: super.alertDialog( key, title, message, releaseNotes, context, cupertino, messages), diff --git a/example/lib/main-appcast.dart b/example/lib/main_appcast.dart similarity index 61% rename from example/lib/main-appcast.dart rename to example/lib/main_appcast.dart index 236ec972..bcba1258 100644 --- a/example/lib/main-appcast.dart +++ b/example/lib/main_appcast.dart @@ -11,31 +11,30 @@ 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), + oniOS: () => 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 85% rename from example/lib/main-custom-alert.dart rename to example/lib/main_custom_alert.dart index 0aff7224..8c69d4ea 100644 --- a/example/lib/main-custom-alert.dart +++ b/example/lib/main_custom_alert.dart @@ -24,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...')), )), ); } @@ -34,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. @@ -66,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/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..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'; @@ -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/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/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..2dce347b 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 @@ -19,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, @@ -37,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; @@ -54,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) @@ -99,29 +99,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(); @@ -130,11 +131,10 @@ 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.debugLogging) { - print('upgrader: shouldDisplayReleaseNotes: shouldDisplayReleaseNotes'); + if (widget.upgrader.state.debugLogging) { + print('upgrader: shouldDisplayReleaseNotes: $shouldDisplayReleaseNotes'); } if (shouldDisplay) { displayed = true; @@ -148,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, ); }); @@ -156,7 +156,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 +173,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 +186,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'); } @@ -218,10 +218,10 @@ 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.debugLogging) { + if (widget.upgrader.state.debugLogging) { print('upgrader: showTheDialog title: $title'); print('upgrader: showTheDialog message: $message'); print('upgrader: showTheDialog releaseNotes: $releaseNotes'); @@ -231,34 +231,39 @@ 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() { - if (widget.upgrader.debugLogging) { - print('upgrader: onWillPop called'); + /// 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: onCanPop 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_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_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/upgrade_state.dart b/lib/src/upgrade_state.dart new file mode 100644 index 00000000..80328749 --- /dev/null +++ b/lib/src/upgrade_state.dart @@ -0,0 +1,136 @@ +// 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 'package:version/version.dart'; + +import 'upgrade_device.dart'; +import 'upgrade_messages.dart'; +import 'upgrade_os.dart'; +import 'upgrader_version_info.dart'; + +/// The [Upgrader] state. +class UpgraderState { + /// Creates an [Upgrader] state. + UpgraderState({ + required this.client, + this.countryCodeOverride, + 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, + required this.upgraderOS, + this.versionInfo, + }); + + /// 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; + + /// 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. Overrides any minimum + /// app version from UpgraderStore. 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, + Duration? durationUntilAlertAgain, + String? languageCodeOverride, + UpgraderMessages? messages, + 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, + durationUntilAlertAgain: + durationUntilAlertAgain ?? this.durationUntilAlertAgain, + languageCodeOverride: languageCodeOverride ?? this.languageCodeOverride, + messages: messages ?? this.messages, + 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? messages, + bool? minAppVersion, + bool? packageInfo, + bool? versionInfo, + }) { + return UpgraderState( + client: client, + countryCodeOverride: + countryCodeOverride == true ? null : this.countryCodeOverride, + 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, + upgraderOS: upgraderOS, + versionInfo: versionInfo == true ? null : this.versionInfo, + ); + } +} diff --git a/lib/src/upgrade_store_controller.dart b/lib/src/upgrade_store_controller.dart new file mode 100644 index 00000000..7ba11c9c --- /dev/null +++ b/lib/src/upgrade_store_controller.dart @@ -0,0 +1,287 @@ +// 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 "$version" 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 "$version" 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, + 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( + {required UpgraderState state, + required Version installedVersion, + required String? country, + required String? language}) async { + String? appStoreListingURL; + Version? appStoreVersion; + bool? isCriticalUpdate; + String? releaseNotes; + + final localAppcast = appcast ?? + Appcast( + client: state.client, + upgraderDevice: state.upgraderDevice, + upgraderOS: state.upgraderOS); + await localAppcast.parseAppcastItemsFromUri(appcastURL); + if (state.debugLogging) { + var count = localAppcast.items == null ? 0 : localAppcast.items!.length; + print('upgrader: UpgraderAppcastStore item count: $count'); + } + final criticalUpdateItem = localAppcast.bestCriticalItem(); + final criticalVersion = criticalUpdateItem?.versionString ?? ''; + + final bestItem = localAppcast.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: getVersionInfo 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; + } +} + +/// 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 e1f5d763..2f925bd7 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,12 @@ 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_device.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(); @@ -25,90 +24,62 @@ typedef BoolCallback = bool Function(); 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}); - -/// 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". -class AppcastConfiguration { - final List? supportedOS; - final String? url; - - AppcastConfiguration({ - this.supportedOS, - this.url, - }); -} +/// installedVersion, and versionInfo. +typedef WillDisplayUpgradeCallback = void Function({ + required bool display, + String? installedVersion, + UpgraderVersionInfo? versionInfo, +}); /// 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.appcastConfig, - this.appcast, - this.messages, - this.debugDisplayAlways = false, - this.debugDisplayOnce = false, - this.debugLogging = false, - this.durationUntilAlertAgain = const Duration(days: 3), - this.willDisplayUpgrade, http.Client? client, - this.countryCode, - this.languageCode, - this.minAppVersion, + String? countryCode, + bool debugDisplayAlways = false, + bool debugDisplayOnce = false, + bool debugLogging = false, + Duration durationUntilAlertAgain = const Duration(days: 3), + String? languageCode, + UpgraderMessages? messages, + String? minAppVersion, + UpgraderStoreController? storeController, + UpgraderDevice? upgraderDevice, UpgraderOS? upgraderOS, - }) : client = client ?? http.Client(), - upgraderOS = upgraderOS ?? UpgraderOS() { - if (debugLogging) print("upgrader: instantiated."); + this.willDisplayUpgrade, + }) : _state = UpgraderState( + client: client ?? http.Client(), + countryCodeOverride: countryCode, + debugDisplayAlways: debugDisplayAlways, + debugDisplayOnce: debugDisplayOnce, + debugLogging: debugLogging, + durationUntilAlertAgain: durationUntilAlertAgain, + languageCodeOverride: languageCode, + messages: messages, + minAppVersion: + parseVersion(minAppVersion, 'minAppVersion', debugLogging), + upgraderDevice: upgraderDevice ?? UpgraderDevice(), + upgraderOS: upgraderOS ?? UpgraderOS(), + ), + storeController = storeController ?? UpgraderStoreController() { + if (_state.debugLogging) { + print("upgrader: instantiated"); + } } - /// Provide an Appcast that can be replaced for mock testing. - final Appcast? appcast; - - /// The appcast configuration ([AppcastConfiguration]) used by [Appcast]. - /// When an appcast is configured for iOS, the iTunes lookup is not used. - final AppcastConfiguration? appcastConfig; - - /// 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; - - /// 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; - - /// Enable print statements for debugging. - bool debugLogging; - - /// 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; + /// The controller that provides the store details for each platform. + 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, @@ -116,67 +87,41 @@ class Upgrader with WidgetsBindingObserver { /// is logging metrics for your app. WillDisplayUpgradeCallback? willDisplayUpgrade; - bool _initCalled = false; - PackageInfo? _packageInfo; + /// A shared instance of [Upgrader]. + static Upgrader get sharedInstance => _sharedInstance; - String? _installedVersion; - String? _appStoreVersion; - String? _appStoreListingURL; - String? _releaseNotes; - String? _updateAvailable; - DateTime? _lastTimeAlerted; - String? _lastVersionAlerted; - String? _userIgnoredVersion; - bool _hasAlerted = false; - bool _isCriticalUpdate = false; + /// 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(); /// 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 shared instance of [Upgrader]. - static Upgrader get sharedInstance => _sharedInstance; + 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 => _appStoreListingURL; - - String? get currentAppStoreVersion => _appStoreVersion; - - String? get currentInstalledVersion => _installedVersion; - - String? get releaseNotes => _releaseNotes; - - void installPackageInfo({PackageInfo? packageInfo}) { - _packageInfo = packageInfo; - _initCalled = false; - } - - void installAppStoreVersion(String version) => _appStoreVersion = version; - - void installAppStoreListingURL(String url) => _appStoreListingURL = url; - /// 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.'); return true; @@ -185,48 +130,58 @@ 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 (state.debugLogging) print('upgrader: ${state.upgraderOS}'); - if (_packageInfo == null) { - _packageInfo = await PackageInfo.fromPlatform(); - if (debugLogging) { - print( - 'upgrader: package info packageName: ${_packageInfo!.packageName}'); - print('upgrader: package info appName: ${_packageInfo!.appName}'); - print('upgrader: package info version: ${_packageInfo!.version}'); + if (state.packageInfo == null) { + try { + final packageInfo = await PackageInfo.fromPlatform(); + updateState(state.copyWith(packageInfo: packageInfo)); + } catch (e) { + if (state.debugLogging) { + print('upgrader: PackageInfo exception: $e'); + } } } - _installedVersion = _packageInfo!.version; + 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(); - // 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; - - /// Trigger the stream to indicate an evaluation should be performed. - /// The value will always be true. - _streamController.add(true); - return true; }); return _futureInit!; } + /// 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(); + } + + /// 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. void dispose() { // Remove the observer of application events. @@ -235,160 +190,84 @@ 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); } } - 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}'); - } - - 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; - } + /// 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; + } - _appStoreVersion = bestItem.versionString; - _appStoreListingURL = bestItem.fileURL; - _releaseNotes = bestItem.itemDescription; - } - } else { - if (_packageInfo == null || _packageInfo!.packageName.isEmpty) { - return false; + // 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; + } - // The country code of the locale, defaulting to `US`. - final country = countryCode ?? findCountryCode(); - if (debugLogging) { - print('upgrader: countryCode: $country'); + // Determine the installed version of this app. + late Version installedVersion; + try { + installedVersion = Version.parse(state.packageInfo!.version); + } catch (e) { + if (state.debugLogging) { + print('upgrader: installedVersion exception: $e'); } + updateState(state.copyWithNull(versionInfo: null)); + return null; + } - // The language code of the locale, defaulting to `en`. - final language = languageCode ?? findLanguageCode(); - if (debugLogging) { - print('upgrader: languageCode: $language'); - } + final locale = findLocale(); - // 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 country code of the locale, defaulting to `US`. + final country = + state.countryCodeOverride ?? findCountryCode(locale: locale); + if (state.debugLogging) { + print('upgrader: countryCode: $country'); } - return true; - } - - /// 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'); - } - } + // Determine the language code of the locale, defaulting to `en`. + final language = + state.languageCodeOverride ?? findLanguageCode(locale: locale); + if (state.debugLogging) { + print('upgrader: languageCode: $language'); } - return true; - } + // Get the version info from the store. + final versionInfo = await store.getVersionInfo( + state: state, + installedVersion: installedVersion, + country: country, + language: language); - bool isAppcastThisPlatform() { - if (appcastConfig == null || - appcastConfig!.url == null || - appcastConfig!.url!.isEmpty) { - return false; - } + updateState(state.copyWith(versionInfo: versionInfo)); - // 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; + return versionInfo; } bool verifyInit() { if (!_initCalled) { - throw ('upgrader: initialize() not called. Must be called first.'); + throw (notInitializedExceptionMessage); } return true; } String appName() { verifyInit(); - return _packageInfo?.appName ?? ''; + return state.packageInfo?.appName ?? ''; } String body(UpgraderMessages messages) { @@ -401,56 +280,22 @@ 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 (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 (debugLogging) { - print('upgrader: languageCode: ${appMessages.languageCode}'); - } - - return appMessages; - } - } - bool blocked() { - return belowMinAppVersion() || _isCriticalUpdate; + return belowMinAppVersion() || versionInfo?.isCriticalUpdate == true; } bool shouldDisplayUpgrade() { final isBlocked = blocked(); - if (debugLogging) { + 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; @@ -459,17 +304,17 @@ class Upgrader with WidgetsBindingObserver { } else if (isTooSoon() || alreadyIgnoredThisVersion()) { rv = false; } - if (debugLogging) { + if (state.debugLogging) { print('upgrader: shouldDisplayUpgrade: $rv'); } // Call the [willDisplayUpgrade] callback when available. if (willDisplayUpgrade != null) { willDisplayUpgrade!( - display: rv, - minAppVersion: minAppVersion, - installedVersion: _installedVersion, - appStoreVersion: _appStoreVersion); + display: rv, + installedVersion: state.packageInfo?.version, + versionInfo: versionInfo, + ); } return rv; @@ -478,13 +323,13 @@ 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 && state.packageInfo != null) { try { - final minVersion = Version.parse(minAppVersion!); - final installedVersion = Version.parse(_installedVersion!); + final installedVersion = Version.parse(state.packageInfo!.version); rv = installedVersion < minVersion; } catch (e) { - if (debugLogging) { + if (state.debugLogging) { print(e); } } @@ -498,78 +343,73 @@ class Upgrader with WidgetsBindingObserver { } final lastAlertedDuration = DateTime.now().difference(_lastTimeAlerted!); - final rv = lastAlertedDuration < durationUntilAlertAgain; - if (rv && debugLogging) { + final rv = lastAlertedDuration < state.durationUntilAlertAgain; + if (rv && state.debugLogging) { print('upgrader: isTooSoon: true'); } return rv; } bool alreadyIgnoredThisVersion() { - final rv = - _userIgnoredVersion != null && _userIgnoredVersion == _appStoreVersion; - if (rv && debugLogging) { + final rv = _userIgnoredVersion != null && + _userIgnoredVersion == versionInfo?.appStoreVersion; + if (rv && state.debugLogging) { print('upgrader: alreadyIgnoredThisVersion: true'); } return rv; } bool isUpdateAvailable() { - if (debugLogging) { - print('upgrader: appStoreVersion: $_appStoreVersion'); - print('upgrader: installedVersion: $_installedVersion'); - print('upgrader: minAppVersion: $minAppVersion'); + if (state.debugLogging) { + print('upgrader: installedVersion: ${state.packageInfo?.version}'); + print('upgrader: minAppVersion: ${state.minAppVersion}'); } - if (_appStoreVersion == null || _installedVersion == null) { - if (debugLogging) print('upgrader: isUpdateAvailable: false'); + if (versionInfo?.appStoreVersion == null || + state.packageInfo?.version == null) { + if (state.debugLogging) print('upgrader: isUpdateAvailable: false'); return false; } try { - final appStoreVersion = Version.parse(_appStoreVersion!); - final installedVersion = Version.parse(_installedVersion!); + final installedVersion = Version.parse(state.packageInfo!.version); - final available = appStoreVersion > installedVersion; - _updateAvailable = available ? _appStoreVersion : null; + 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; } - /// 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; } @@ -582,11 +422,34 @@ 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(); - _userIgnoredVersion = _appStoreVersion; - await prefs.setString('userIgnoredVersion', _userIgnoredVersion ?? ''); + _userIgnoredVersion = versionInfo?.appStoreVersion; + await prefs.setString( + 'userIgnoredVersion', _userIgnoredVersion?.toString() ?? ''); return true; } @@ -595,8 +458,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; @@ -608,37 +472,108 @@ class Upgrader with WidgetsBindingObserver { if (lastTimeAlerted != null) { _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 (state.debugLogging) { + print('upgrader: lastVersionAlerted exception: $e'); + } + } + } + final ignoredVersion = prefs.getString('userIgnoredVersion'); + if (ignoredVersion != null) { + try { + _userIgnoredVersion = Version.parse(ignoredVersion); + } catch (e) { + if (state.debugLogging) { + print('upgrader: userIgnoredVersion exception: $e'); + } + } + } return true; } + /// Launch the app store from the app store listing URL. void sendUserToAppStore() async { - if (_appStoreListingURL == null || _appStoreListingURL!.isEmpty) { - if (debugLogging) { - print('upgrader: empty _appStoreListingURL'); + final appStoreListingURL = versionInfo?.appStoreListingURL; + if (appStoreListingURL == null || appStoreListingURL.isEmpty) { + if (state.debugLogging) { + print('upgrader: empty appStoreListingURL'); } return; } - if (debugLogging) { - print('upgrader: launching: $_appStoreListingURL'); + if (state.debugLogging) { + print('upgrader: launching: $appStoreListingURL'); } - if (await canLaunchUrl(Uri.parse(_appStoreListingURL!))) { + if (await canLaunchUrl(Uri.parse(appStoreListingURL))) { try { - await launchUrl(Uri.parse(_appStoreListingURL!), - mode: upgraderOS.isAndroid + await launchUrl(Uri.parse(appStoreListingURL), + mode: state.upgraderOS.isAndroid ? LaunchMode.externalNonBrowserApplication : LaunchMode.platformDefault); } catch (e) { - if (debugLogging) { + if (state.debugLogging) { 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/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/pubspec.yaml b/pubspec.yaml index f1325ed2..d5d6a4b7 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: 9.0.0 +version: 10.0.0 homepage: https://github.com/larryaasen/upgrader environment: sdk: '>=3.1.0 <4.0.0' - flutter: ">=3.13.1" + flutter: ">=3.16.0" dependencies: flutter: 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/fake_appcast.dart b/test/fake_appcast.dart index 493422a7..cf329547 100644 --- a/test/fake_appcast.dart +++ b/test/fake_appcast.dart @@ -5,11 +5,12 @@ import 'dart:io'; import 'package:mockito/mockito.dart'; import 'package:upgrader/src/appcast.dart'; -import 'package:upgrader/src/upgrader.dart'; +import 'package:upgrader/src/upgrade_device.dart'; import 'appcast_test.dart'; class FakeAppcast extends Fake implements TestAppcast { + FakeAppcast(); int callCount = 0; @override @@ -55,16 +56,19 @@ class FakeAppcast extends Fake implements TestAppcast { return [AppcastItem()]; } - AppcastConfiguration config = - AppcastConfiguration(url: 'http://some.fakewebsite.com', supportedOS: [ - 'android', - 'fuchsia', - 'ios', - 'linux', - 'macos', - 'web', - 'windows', - ]); + @override + UpgraderDevice get upgraderDevice => MockUpgraderDevice(); + + // AppcastConfiguration config = + // AppcastConfiguration(url: 'http://some.fakewebsite.com', supportedOS: [ + // 'android', + // 'fuchsia', + // 'ios', + // 'linux', + // 'macos', + // 'web', + // 'windows', + // ]); @override List? items = []; diff --git a/test/mock_itunes_client.dart b/test/mock_itunes_client.dart index 99c08558..2a9800fc 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 } ] @@ -85,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..26ca99b2 100644 --- a/test/mock_play_store_client.dart +++ b/test/mock_play_store_client.dart @@ -14,6 +14,9 @@ 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', + '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 a22c9ebb..cc74fed5 100644 --- a/test/play_store_test.dart +++ b/test/play_store_test.dart @@ -127,6 +127,42 @@ 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); + + 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 +205,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/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/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 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/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_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 new file mode 100644 index 00000000..7bdae9c5 --- /dev/null +++ b/test/upgrader_store_controller_test.dart @@ -0,0 +1,123 @@ +// 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( + debugLogging: true, + 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( + debugLogging: true, + 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( + debugLogging: true, + 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); + }); +} diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 4b455da2..91b581f4 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'; @@ -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'; @@ -77,13 +78,55 @@ 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); + 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), @@ -111,26 +154,30 @@ 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); - 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); @@ -143,6 +190,7 @@ void main() { await upgrader.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(upgrader.isUpdateAvailable(), true); + expect(upgrader.currentAppStoreVersion, '7.0.0'); upgrader.installPackageInfo( packageInfo: PackageInfo( @@ -153,17 +201,19 @@ void main() { await upgrader.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(upgrader.isUpdateAvailable(), false); - }); - }); - 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.installPackageInfo( + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.larryaasen.upgrader.4', + version: '1.9.9', + buildNumber: '400')); - expect(upgrader.currentAppStoreListingURL, - 'https://itunes.apple.com/us/app/google-maps-transit-food/id585027354?mt=8&uo=4'); - }, skip: false); + await upgrader.didChangeAppLifecycleState(AppLifecycleState.resumed); + expect(upgrader.currentAppStoreVersion, isNull); + expect(upgrader.isUpdateAvailable(), false); + }); + }); testWidgets('test UpgradeAlert', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); @@ -184,21 +234,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; @@ -233,24 +285,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); // }); @@ -281,19 +336,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; @@ -328,11 +384,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) => @@ -340,16 +396,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); @@ -373,9 +432,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; @@ -402,9 +461,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); @@ -428,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; var notCalled = true; @@ -457,9 +516,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); @@ -482,9 +541,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( @@ -568,13 +627,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', @@ -595,7 +655,28 @@ 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 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', @@ -618,7 +699,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 { @@ -664,217 +745,216 @@ 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); 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, - appcastConfig: fakeAppcast.config, - appcast: fakeAppcast) - ..installPackageInfo( - packageInfo: PackageInfo( - appName: 'Upgrader', - packageName: 'com.larryaasen.upgrader', - version: '1.9.6', - buildNumber: '42', - ), - ); + 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', + ), + ); - 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'); - final appcast = Appcast( - client: mockClient, - upgraderOS: upgraderOS, - upgraderDevice: MockUpgraderDevice()); + 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( - upgraderOS: upgraderOS, - debugLogging: true, - appcastConfig: AppcastConfiguration( - url: '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, ), - appcast: appcast, - )..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? minAppVersion, - String? installedVersion, - String? appStoreVersion}) { - expect(display, true); - expect(installedVersion, '1.9.6'); - - /// Appcast Test critical version. - expect(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 appcast = Appcast( - client: mockClient, - upgraderOS: upgraderOS, - upgraderDevice: MockUpgraderDevice()); + final Client mockClient = + setupMockClient(filePath: 'test/testappcastmulti.xml'); - final upgrader = Upgrader( - upgraderOS: upgraderOS, - debugLogging: true, - appcastConfig: AppcastConfiguration( - url: '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', ), - appcast: appcast, - )..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? minAppVersion, - String? installedVersion, - String? appStoreVersion}) { - expect(display, true); - expect(installedVersion, '1.9.6'); - expect(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.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)); + test('durationUntilAlertAgain is 0 days', () async { + final upgrader = + Upgrader(durationUntilAlertAgain: const Duration(seconds: 0)); + expect(upgrader.state.durationUntilAlertAgain, const Duration(seconds: 0)); - UpgradeAlert(upgrader: upgrader); - expect(upgrader.durationUntilAlertAgain, const Duration(seconds: 0)); + UpgradeAlert(upgrader: upgrader); + expect(upgrader.state.durationUntilAlertAgain, const Duration(seconds: 0)); - UpgradeCard(upgrader: upgrader); - expect(upgrader.durationUntilAlertAgain, const Duration(seconds: 0)); - }, skip: false); + UpgradeCard(upgrader: upgrader); + 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)); + test('durationUntilAlertAgain card is valid', () async { + final upgrader = Upgrader(durationUntilAlertAgain: const Duration(days: 3)); + UpgradeCard(upgrader: upgrader); + 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)); - }, skip: false); + final upgrader2 = + Upgrader(durationUntilAlertAgain: const Duration(days: 10)); + UpgradeCard(upgrader: upgrader2); + 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)); + test('durationUntilAlertAgain alert is valid', () async { + final upgrader = Upgrader(durationUntilAlertAgain: const Duration(days: 3)); + UpgradeAlert(upgrader: upgrader); + 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)); - }, skip: false); - }); + final upgrader2 = + Upgrader(durationUntilAlertAgain: const Duration(days: 10)); + UpgradeAlert(upgrader: upgrader2); + expect(upgrader2.state.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 var notCalled = true; - upgrader.willDisplayUpgrade = ( - {required bool display, - String? minAppVersion, - String? installedVersion, - String? appStoreVersion}) { + upgrader.willDisplayUpgrade = ({ + required bool display, + String? installedVersion, + 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); expect(notCalled, false); - upgrader.debugDisplayAlways = true; + upgrader.updateState(upgrader.state.copyWith(debugDisplayAlways: true)); notCalled = true; - upgrader.willDisplayUpgrade = ( - {required bool display, - String? minAppVersion, - String? installedVersion, - String? appStoreVersion}) { + upgrader.willDisplayUpgrade = ({ + required bool display, + String? installedVersion, + 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 +978,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, + UpgraderVersionInfo? versionInfo, + }) { expect(display, true); - expect(minAppVersion, '2.0.0'); + expect(versionInfo!.minAppVersion, isNull); expect(upgrader.minAppVersion, '2.0.0'); expect(installedVersion, '1.9.6'); - expect(appStoreVersion, '5.6'); + expect(versionInfo.appStoreVersion.toString(), '5.6.0'); notCalled = false; }; 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); + }); +}