diff --git a/example/lib/main.dart b/example/lib/main.dart index d2fac69..b22cb42 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -41,22 +41,36 @@ class _HomeState extends State { bool isLoading = false; + int _currentIndex = 0; + @override void initState() { super.initState(); - _getAppVersioning(); + _startVersioningServices(); } - void _getAppVersioning() async { + void _startVersioningServices() async { setState(() { isLoading = true; }); + await Future.wait( + [ + _getAppVersioning(), + _trackVersions(), + ], + ); + + setState(() { + isLoading = false; + }); + } + + Future _getAppVersioning() async { // Get Api Versioning (just to show on screen) final appUpdateInfo = await widget.appVersioning.getAppUpdateInfo(); setState(() { this.appUpdateInfo = appUpdateInfo; - this.isLoading = false; }); // Check Update Required @@ -67,6 +81,10 @@ class _HomeState extends State { } } + Future _trackVersions() async { + await widget.appVersioning.tracker.track(); + } + _showUpdatePopup() { showDialog( context: context, @@ -75,7 +93,7 @@ class _HomeState extends State { child: Column( children: [ Text("Update required!"), - FlatButton( + TextButton( onPressed: () => widget.appVersioning.launchUpdate(), child: Text("OK"), ) @@ -89,36 +107,95 @@ class _HomeState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('App Versioning Example')), - body: Stack( + body: IndexedStack( + index: _currentIndex, children: [ - Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Current Versioning Values', - style: Theme.of(context).textTheme.headline6, - ), - Text( - 'Current Version: ${appUpdateInfo?.currentVersion}', + Stack( + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Current Versioning Values', + style: Theme.of(context).textTheme.headline6, + ), + Text( + 'Current Version: ${appUpdateInfo?.currentVersion}', + ), + Text( + 'Minimum Version: ${appUpdateInfo?.minimumVersion}', + ), + Text( + 'Update Available: ${appUpdateInfo?.isUpdateAvailable}', + ), + Text( + 'Update Type: ${appUpdateInfo?.updateType}', + ), + ], ), - Text( - 'Minimum Version: ${appUpdateInfo?.minimumVersion}', + ), + if (isLoading) + Center( + child: CircularProgressIndicator(), ), - Text( - 'Update Available: ${appUpdateInfo?.isUpdateAvailable}', + ], + ), + Stack( + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Version Tracking Status', + style: Theme.of(context).textTheme.headline6, + ), + Text( + 'Is first launch ever: ${widget.appVersioning.tracker.isFirstLaunchEver}', + ), + Text( + 'Is first launch for current version: ${widget.appVersioning.tracker.isFirstLaunchForCurrentVersion}', + ), + Text( + 'Is first launch for current build: ${widget.appVersioning.tracker.isFirstLaunchForCurrentBuild}', + ), + Text( + 'Version history: ${widget.appVersioning.tracker.versionHistory}', + ), + ], ), - Text( - 'Update Type: ${appUpdateInfo?.updateType}', + ), + if (isLoading) + Center( + child: CircularProgressIndicator(), ), - ], + ], + ) + ], + ), + bottomNavigationBar: BottomNavigationBar( + currentIndex: _currentIndex, + onTap: (index) => setState( + () { + _currentIndex = index; + }, + ), + items: [ + BottomNavigationBarItem( + icon: Icon( + Icons.analytics, ), + label: "App versioning", + ), + BottomNavigationBarItem( + icon: Icon(Icons.history), + label: "Version tracker", ), - if (isLoading) Center(child: CircularProgressIndicator()), ], ), floatingActionButton: FloatingActionButton( - onPressed: () => _getAppVersioning(), + onPressed: _startVersioningServices, tooltip: 'Refresh', child: Icon(Icons.refresh), ), diff --git a/example/pubspec.lock b/example/pubspec.lock index c312c78..e06bbbe 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -50,6 +50,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" firebase_core: dependency: transitive description: @@ -158,6 +172,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" pedantic: dependency: transitive description: @@ -165,6 +200,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.11.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" plugin_platform_interface: dependency: transitive description: @@ -172,6 +214,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" quiver: dependency: transitive description: @@ -179,6 +228,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.5" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -282,6 +373,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" sdks: dart: ">=2.12.0 <3.0.0" flutter: ">=1.22.0" diff --git a/lib/app_versioning.dart b/lib/app_versioning.dart index cfadd44..44db9fc 100644 --- a/lib/app_versioning.dart +++ b/lib/app_versioning.dart @@ -13,6 +13,7 @@ import 'package:lr_app_versioning/src/model/app_update_info.dart'; import 'package:lr_app_versioning/src/service/device_versioning_service.dart'; import 'package:lr_app_versioning/src/service/minimum_versioning_service.dart'; import 'package:lr_app_versioning/src/util/version.dart'; +import 'package:lr_app_versioning/src/util/version_tracker.dart'; // Library Export Classes export 'src/api/exports.dart'; @@ -26,6 +27,8 @@ export 'src/service/minimum_versioning_service.dart'; export 'src/util/version.dart'; abstract class AppVersioning { + VersionTracker get tracker; + Future getCurrentAppVersion(); Future getAppUpdateInfo(); diff --git a/lib/src/default_app_versioning.dart b/lib/src/default_app_versioning.dart index e9337f0..74359bb 100644 --- a/lib/src/default_app_versioning.dart +++ b/lib/src/default_app_versioning.dart @@ -5,6 +5,7 @@ import 'package:lr_app_versioning/src/model/app_update_info.dart'; import 'package:lr_app_versioning/src/service/device_versioning_service.dart'; import 'package:lr_app_versioning/src/service/minimum_versioning_service.dart'; import 'package:lr_app_versioning/src/util/version.dart'; +import 'package:lr_app_versioning/src/util/version_tracker.dart'; class DefaultAppVersioning implements AppVersioning { final MinimumVersioningService _minimumVersioningService; @@ -15,6 +16,9 @@ class DefaultAppVersioning implements AppVersioning { required DeviceVersioningService appUpdateService, }) : _minimumVersioningService = minimumVersioningService, _appUpdateService = appUpdateService; + + @override + VersionTracker get tracker => VersionTracker.instance; @override Future getCurrentAppVersion() { diff --git a/lib/src/model/api_versioning.g.dart b/lib/src/model/api_versioning.g.dart deleted file mode 100644 index 8685e81..0000000 --- a/lib/src/model/api_versioning.g.dart +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'api_versioning.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -ApiVersioning _$ApiVersioningFromJson(Map json) { - return ApiVersioning( - minimumIosVersionString: json['minimumIosVersion'] as String, - minimumAndroidVersionString: json['minimumAndroidVersion'] as String, - ); -} - -Map _$ApiVersioningToJson(ApiVersioning instance) => - { - 'minimumIosVersion': instance.minimumIosVersionString, - 'minimumAndroidVersion': instance.minimumAndroidVersionString, - }; diff --git a/lib/src/util/version_tracker.dart b/lib/src/util/version_tracker.dart new file mode 100644 index 0000000..cd02c08 --- /dev/null +++ b/lib/src/util/version_tracker.dart @@ -0,0 +1,163 @@ +import 'package:package_info/package_info.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Provides an easy way to track versions and builds +class VersionTracker { + VersionTracker._internal(); + + static final VersionTracker _instance = VersionTracker._internal(); + static VersionTracker get instance => _instance; + + final String _versionsKey = "VersionTracker.Versions"; + final String _buildsKey = "VersionTracker.Builds"; + + /// Gets a value indicating whether this is the first time this app has ever been launched on this device. + bool? get isFirstLaunchEver => _isFirstLaunchEver; + bool? _isFirstLaunchEver; + + /// Gets a value indicating if this is the first launch of the app for the current version number. + bool? get isFirstLaunchForCurrentVersion => _isFirstLaunchForCurrentVersion; + bool? _isFirstLaunchForCurrentVersion; + + /// Gets a value indicating if this is the first launch of the app for the current build number. + bool? get isFirstLaunchForCurrentBuild => _isFirstLaunchForCurrentBuild; + bool? _isFirstLaunchForCurrentBuild; + + /// Gets the current version number of the app. + String? get currentVersion => _currentVersion; + String? _currentVersion; + + /// Gets the current build of the app. + String? get currentBuild => _currentBuild; + String? _currentBuild; + + /// Gets the version number for the previously run version. + String? get previousVersion => _previousVersion; + String? _previousVersion; + + /// Gets the build number for the previously run version. + String? get previousBuild => _previousBuild; + String? _previousBuild; + + /// Gets the version number of the first version of the app that was installed on this device. + String? get firstInstalledVersion => _firstInstalledVersion; + String? _firstInstalledVersion; + + /// Gets the build number of first version of the app that was installed on this device. + String? get firstInstalledBuild => _firstInstalledBuild; + String? _firstInstalledBuild; + + /// Gets the collection of version numbers of the app that ran on this device. + List? get versionHistory => _versionHistory; + List? _versionHistory; + + /// Gets the collection of build numbers of the app that ran on this device. + List? get buildHistory => _buildHistory; + List? _buildHistory; + + /// Determines if this is the first launch of the app for a specified version number. + bool isFirstLaunchForVersion(String version) => + _currentVersion == version && _isFirstLaunchForCurrentVersion!; + + /// Determines if this is the first launch of the app for a specified build number. + bool isFirstLaunchForBuild(String build) => + _currentBuild == build && _isFirstLaunchForCurrentBuild!; + + /// Start tracking versions and builds + Future track( + {int? versionHistoryMaxLength, int? buildHistoryMaxLength}) async { + SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + + Map> historyTrail = Map>(); + + _isFirstLaunchEver = !sharedPreferences.containsKey(_versionsKey) || + !sharedPreferences.containsKey(_buildsKey); + if (_isFirstLaunchEver!) + historyTrail.addAll({_versionsKey: [], _buildsKey: []}); + else + historyTrail.addAll({ + _versionsKey: _readHistory(sharedPreferences, _versionsKey).toList(), + _buildsKey: _readHistory(sharedPreferences, _buildsKey).toList() + }); + + // Handle versions + _currentVersion = packageInfo.version; + + _isFirstLaunchForCurrentVersion = + !historyTrail[_versionsKey]!.contains(_currentVersion); + if (_isFirstLaunchForCurrentVersion!) + historyTrail[_versionsKey]!.add(_currentVersion); + + if (versionHistoryMaxLength != null && versionHistoryMaxLength > 0) { + var versionsToRemove = + historyTrail[_versionsKey]!.length - versionHistoryMaxLength; + if (versionsToRemove > 0) + historyTrail[_versionsKey]!.removeRange(1, versionsToRemove + 1); + } + + _previousVersion = _getPrevious(historyTrail, _versionsKey); + _firstInstalledVersion = historyTrail[_versionsKey]!.first; + _versionHistory = historyTrail[_versionsKey]!.toList(); + + // Handle builds + _currentBuild = packageInfo.buildNumber; + + _isFirstLaunchForCurrentBuild = + !historyTrail[_buildsKey]!.contains(_currentBuild); + if (_isFirstLaunchForCurrentBuild!) + historyTrail[_buildsKey]!.add(_currentBuild); + + if (buildHistoryMaxLength != null && buildHistoryMaxLength > 0) { + var buildsToRemove = + historyTrail[_buildsKey]!.length - buildHistoryMaxLength; + if (buildsToRemove > 0) + historyTrail[_buildsKey]!.removeRange(1, buildsToRemove + 1); + } + + if (_isFirstLaunchForCurrentVersion! || _isFirstLaunchForCurrentBuild!) { + _writeHistory( + sharedPreferences, _versionsKey, historyTrail[_versionsKey]!); + _writeHistory(sharedPreferences, _buildsKey, historyTrail[_buildsKey]!); + } + + _previousBuild = _getPrevious(historyTrail, _buildsKey); + _firstInstalledBuild = historyTrail[_buildsKey]!.first; + _buildHistory = historyTrail[_buildsKey]!.toList(); + } + + /// Show all the available data in a formatted string + @override + String toString() { + var sb = StringBuffer(); + sb.writeln('VersionTracker'); + sb.writeln(); + sb.writeln('IsFirstLaunchEver: $_isFirstLaunchEver'); + sb.writeln( + 'IsFirstLaunchForCurrentVersion: $_isFirstLaunchForCurrentVersion'); + sb.writeln('IsFirstLaunchForCurrentBuild: $_isFirstLaunchForCurrentBuild'); + sb.writeln(); + sb.writeln('CurrentVersion: $_currentVersion'); + sb.writeln('PreviousVersion: $_previousVersion'); + sb.writeln('FirstInstalledVersion: $_firstInstalledVersion'); + sb.writeln('VersionHistory: ${_versionHistory!.join(", ")}'); + sb.writeln(); + sb.writeln('CurrentBuild: $_currentBuild'); + sb.writeln('PreviousBuild: $_previousBuild'); + sb.writeln('FirstInstalledBuild: $_firstInstalledBuild'); + sb.writeln('BuildHistory: ${_buildHistory!.join(", ")}'); + return sb.toString(); + } + + List _readHistory(SharedPreferences preferences, String key) => + preferences.getString(key)!.split('|'); + + void _writeHistory(SharedPreferences preferences, String key, + List historyTrail) => + preferences.setString(key, historyTrail.join('|')); + + String? _getPrevious(Map> historyTrail, String key) { + var trail = historyTrail[key]!; + return (trail.length >= 2) ? trail[trail.length - 2] : null; + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 9c3e5dd..e6a98db 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -50,6 +50,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" firebase_core: dependency: transitive description: @@ -151,6 +165,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" pedantic: dependency: transitive description: @@ -158,6 +193,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.11.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" plugin_platform_interface: dependency: transitive description: @@ -165,6 +207,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" quiver: dependency: transitive description: @@ -172,6 +221,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.5" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -275,6 +366,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" sdks: dart: ">=2.12.0 <3.0.0" flutter: ">=1.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 477cb93..90af75d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,8 @@ dependencies: package_info: 2.0.0 # URL Launcher url_launcher: 6.0.3 + # Shared Preferences + shared_preferences: 2.0.5 # Firebase Remote Config firebase_remote_config: 0.4.3