diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1..389b2d0 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,3 +2,7 @@ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options + +linter: + rules: + - prefer_double_quotes diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index b8f28ed..8c09e03 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,8 +7,10 @@ import Foundation import device_info_plus import package_info_plus +import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/example/pubspec.lock b/example/pubspec.lock index 6ebd026..4a1a1e0 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: path: ".." relative: true source: path - version: "0.2.0" + version: "0.3.0" async: dependency: transitive description: @@ -48,6 +48,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -105,10 +113,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "4.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -119,6 +127,22 @@ packages: description: flutter source: sdk version: "0.0.0" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: transitive + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" http: dependency: transitive description: @@ -163,10 +187,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "4.0.0" matcher: dependency: transitive description: @@ -215,6 +239,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" + url: "https://pub.dev" + source: hosted + version: "2.2.5" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -332,6 +412,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.0" + flutter: ">=3.22.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 996eff1..a357626 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -19,7 +19,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.1.0 <4.0.0' + sdk: '>=3.2.0 <4.0.0' + flutter: ">=1.17.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -47,7 +48,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.0 + flutter_lints: ^4.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/lib/aptabase_flutter.dart b/lib/aptabase_flutter.dart index daf97c5..cbe0e5b 100644 --- a/lib/aptabase_flutter.dart +++ b/lib/aptabase_flutter.dart @@ -1,66 +1,165 @@ -/// The Flutter SDK for Aptabase, a privacy-first and simple analytics platform for apps. +/// The Flutter SDK for Aptabase, a privacy-first and +/// simple analytics platform for apps. library aptabase_flutter; -import 'dart:convert'; -import 'dart:developer' as developer; -import 'dart:math'; +import "dart:async"; +import "dart:convert"; +import "dart:developer" as developer; -import 'package:aptabase_flutter/sys_info.dart'; -import 'package:flutter/foundation.dart'; -import 'package:universal_io/io.dart'; +import "package:aptabase_flutter/sys_info.dart"; +import "package:flutter/foundation.dart"; +import "package:universal_io/io.dart"; -/// Additional options for initializing the Aptabase SDK. -class InitOptions { - final String? host; +import "package:aptabase_flutter/init_options.dart"; +import "package:aptabase_flutter/random_string.dart"; +import "package:aptabase_flutter/storage_manager.dart"; +import "package:aptabase_flutter/storage_manager_hive.dart"; +import "package:flutter/scheduler.dart"; +import "package:flutter/services.dart"; - const InitOptions({this.host}); -} +export "package:aptabase_flutter/init_options.dart"; + +enum _SendResult { disabled, success, discard, tryAgain } /// Aptabase Client for Flutter /// -/// Initialize the client with `Aptabase.init(appKey)` and then use `Aptabase.instance.trackEvent(eventName, props)` to record events. +/// Initialize the client with `Aptabase.init(appKey)` and then +/// use `Aptabase.instance.trackEvent(eventName, props)` to record events. class Aptabase { - static const String _sdkVersion = "aptabase_flutter@0.3.0"; - static const Duration _sessionTimeout = Duration(hours: 1); + Aptabase._(); + + static const _sdkVersion = "aptabase_flutter@0.3.0"; + static const _sessionTimeout = Duration(hours: 1); static const Map _hosts = { - 'EU': "https://eu.aptabase.com", - 'US': "https://us.aptabase.com", - 'DEV': "http://localhost:3000", - 'SH': "" + "EU": "https://eu.aptabase.com", + "US": "https://us.aptabase.com", + "DEV": "http://localhost:3000", + "SH": "", }; - static final http = newUniversalHttpClient(); - static final rnd = Random(); - static SystemInfo? _sysInfo; - static String _appKey = ""; - static Uri? _apiUrl; - static String _sessionId = newSessionId(); - static DateTime _lastTouchTs = DateTime.now().toUtc(); + static final _http = newUniversalHttpClient(); - Aptabase._(); + static late final String _appKey; + static late final InitOptions _initOptions; + static late final Uri? _apiUrl; + static var _sessionId = _newSessionId(); + static var _lastTouchTs = DateTime.now().toUtc(); + static Timer? _timer; + static var _isTimerRunning = false; + static late final StorageManager _storage; + + static final _inactiveState = AppLifecycleState.inactive.toString(); + static final _pausedState = AppLifecycleState.paused.toString(); static final instance = Aptabase._(); /// Initializes the Aptabase SDK with the given appKey. - static Future init(String appKey, [InitOptions? opts]) async { - _appKey = appKey; + static Future init( + String appKey, [ + InitOptions opts = const InitOptions(), + StorageManager? storage, + ]) async { + final parts = appKey.split("-"); + assert( + parts.length == 3, + "The Aptabase App Key has the pattern A-REG-0000000000", + ); + assert( + _hosts.containsKey(parts[1]), + "The region part must be one of: ${_hosts.keys.join(" ")}", + ); - var parts = _appKey.split("-"); if (parts.length != 3 || _hosts[parts[1]] == null) { - developer.log( - 'The Aptabase App Key "$_appKey" is invalid. Tracking will be disabled.'); + _logError( + "The Aptabase App Key '$_appKey' is invalid. " + "Tracking will be disabled.", + ); + return; } - _sysInfo = await SystemInfo.get(); - if (_sysInfo == null) { - developer.log( - 'This environment is not supported by Aptabase SDK. Tracking will be disabled.'); + _appKey = appKey; + _initOptions = opts; + + final region = parts[1]; + _apiUrl = _getApiUrl(region, opts); + + if (_apiUrl == null) return; + + _logDebug("API URL is defined: $_apiUrl"); + + _storage = storage ?? HiveStorage(); + // OR _storage = storage ?? SharedPrefsStorage(); + + await _storage.init(); + _logDebug("Storage initialized"); + + SystemChannels.lifecycle.setMessageHandler(_handleLifeCycle); + + await _tick("init"); + _startTimer(); + + _logInfo("Aptabase initilized!"); + } + + static void _dispose() { + _timer?.cancel(); + _timer = null; + _isTimerRunning = false; + } + + static void _startTimer() { + _timer ??= Timer.periodic( + _initOptions.tickDuration, + (_) async => _tick("timer"), + ); + } + + static Future _handleLifeCycle(String? msg) async { + if (msg == _inactiveState || msg == _pausedState) { + await _tick("lifecycle $msg"); + _dispose(); + } else { + _startTimer(); + } + + return msg; + } + + static Future _tick(String reason) async { + _logDebug("Checking events ($reason)"); + + if (_isTimerRunning) { + _logDebug("Already running, avoid duplication"); return; } - var region = parts[1]; - _apiUrl = _getApiUrl(region, opts); + try { + _isTimerRunning = true; + + final items = await _storage.getItems(_initOptions.batchLength); + + if (items.isEmpty) return; + + final events = items.map((e) => e.value).toList(); + final result = await _send(events); + + switch (result) { + case _SendResult.disabled: + _dispose(); + + case _SendResult.tryAgain: + break; + + case _SendResult.success: + case _SendResult.discard: + await _storage.deleteAllKeys(items.map((e) => e.key)); + } + } catch (e, s) { + _logError("Error on send events: $e", e, s); + } finally { + _isTimerRunning = false; + } } /// Returns the session id for the current session. @@ -69,87 +168,148 @@ class Aptabase { final now = DateTime.now().toUtc(); final elapsed = now.difference(_lastTouchTs); if (elapsed > _sessionTimeout) { - _sessionId = newSessionId(); + _sessionId = _newSessionId(); + _logDebug("New session ID was generated: $_sessionId"); } _lastTouchTs = now; return _sessionId; } + Future> _systemProps() async { + final sysInfo = await SystemInfo.get(); + + return { + "isDebug": kDebugMode, + "osName": sysInfo.osName, + "osVersion": sysInfo.osVersion, + "locale": sysInfo.locale, + "appVersion": sysInfo.appVersion, + "appBuildNumber": sysInfo.buildNumber, + "sdkVersion": _sdkVersion, + }; + } + /// Records an event with the given name and optional properties. Future trackEvent( String eventName, [ Map? props, ]) async { - if (_appKey.isEmpty || _apiUrl == null || _sysInfo == null) { + if (_appKey.isEmpty || _apiUrl == null) { + _logInfo("Tracking is disabled!"); + return; } + final body = json.encode({ + "timestamp": DateTime.now().toUtc().toIso8601String(), + "sessionId": _evalSessionId(), + "eventName": eventName, + "systemProps": await _systemProps(), + "props": props, + }); + + await _storage.add(body); + } + + static Future<_SendResult> _send(List events) async { try { - final request = await http.postUrl(_apiUrl!); - request.headers.set("App-Key", _appKey); - request.headers.set( - HttpHeaders.contentTypeHeader, "application/json; charset=UTF-8"); + final apiUrl = _apiUrl; + if (apiUrl == null) { + _logInfo("Tracking is disabled!"); + + return _SendResult.disabled; + } + + final request = await _http.postUrl(apiUrl); + + request.followRedirects = true; + request.headers + ..set("App-Key", _appKey) + ..set( + HttpHeaders.contentTypeHeader, + "application/json; charset=UTF-8", + ); if (!kIsWeb) { request.headers.set(HttpHeaders.userAgentHeader, _sdkVersion); } - final systemProps = { - "isDebug": kDebugMode, - "osName": _sysInfo!.osName, - "osVersion": _sysInfo!.osVersion, - "locale": _sysInfo!.locale, - "appVersion": _sysInfo!.appVersion, - "appBuildNumber": _sysInfo!.buildNumber, - "sdkVersion": _sdkVersion, - }; - - final body = json.encode({ - "timestamp": DateTime.now().toUtc().toIso8601String(), - "sessionId": _evalSessionId(), - "eventName": eventName, - "systemProps": systemProps, - "props": props, - }); - - request.write(body); + request.write(events); final response = await request.close(); + _logDebug("Sending ${events.length} events"); + if (kDebugMode && response.statusCode >= 300) { final body = await response.transform(utf8.decoder).join(); - developer.log( - 'trackEvent failed with status code ${response.statusCode}: $body'); - } - } on Exception catch (e, st) { - if (kDebugMode) { - developer.log('Exception $e: $st'); + + _logError( + "TrackEvent failed with status code " + "${response.statusCode}. Response: $body", + ); + + return response.statusCode >= 500 + ? _SendResult.tryAgain + : _SendResult.discard; } + + _logDebug("Sent successfully"); + + return _SendResult.success; + } on Exception catch (e, s) { + _logError("TrackEvent Exception: $e", e, s); + + return _SendResult.tryAgain; } } /// Returns the API URL for the given region. - static Uri? _getApiUrl(String region, InitOptions? opts) { - var baseUrl = _hosts[region]!; + static Uri? _getApiUrl(String region, InitOptions opts) { + var baseUrl = _hosts[region]; + if (region == "SH") { - if (opts?.host != null) { - baseUrl = opts!.host!; + if (opts.host != null) { + baseUrl = opts.host; } else { - developer.log( - 'Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.'); + _logError( + "Host parameter must be defined when using Self-Hosted App Key. " + "Tracking will be disabled.", + ); return null; } } - return Uri.parse('$baseUrl/api/v0/event'); + return Uri.parse("$baseUrl/api/v0/events"); } /// Returns a new session id. - static String newSessionId() { - String epochInSeconds = - (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(); - String random = (rnd.nextInt(100000000)).toString().padLeft(8, '0'); + static String _newSessionId() => RandomString.randomize(); + + static void _logError(String msg, [Object? error, StackTrace? stackTrace]) { + developer.log( + msg, + name: "Aptabase", + level: 1000, + error: error, + stackTrace: stackTrace, + ); + } + + static void _logInfo(String msg) { + developer.log( + msg, + name: "Aptabase", + level: 800, + ); + } + + static void _logDebug(String msg) { + if (!_initOptions.printDebugMessages) return; - return epochInSeconds + random; + developer.log( + msg, + name: "Aptabase", + level: 500, + ); } } diff --git a/lib/init_options.dart b/lib/init_options.dart new file mode 100644 index 0000000..6af3565 --- /dev/null +++ b/lib/init_options.dart @@ -0,0 +1,17 @@ +const _kDefaultTickDuration = Duration(seconds: 30); +const _kMaxBatchLength = 25; + +/// Additional options for initializing the Aptabase SDK. +class InitOptions { + const InitOptions({ + this.host, + this.tickDuration = _kDefaultTickDuration, + this.batchLength = _kMaxBatchLength, + this.printDebugMessages = false, + }) : assert(batchLength <= _kMaxBatchLength, "Maximum is $_kMaxBatchLength"); + + final String? host; + final Duration tickDuration; + final int batchLength; + final bool printDebugMessages; +} diff --git a/lib/random_string.dart b/lib/random_string.dart new file mode 100644 index 0000000..37e4328 --- /dev/null +++ b/lib/random_string.dart @@ -0,0 +1,12 @@ +import "dart:math"; + +class RandomString { + static final _rnd = Random(); + + static String randomize() { + final epochInSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000; + final random = _rnd.nextInt(100000000).toString().padLeft(8, "0"); + + return "$epochInSeconds$random"; + } +} diff --git a/lib/storage_manager.dart b/lib/storage_manager.dart new file mode 100644 index 0000000..43c2a2a --- /dev/null +++ b/lib/storage_manager.dart @@ -0,0 +1,9 @@ +abstract class StorageManager { + Future init() async {} + + Future>> getItems(int length); + + Future add(String item); + + Future deleteAllKeys(Iterable keys); +} diff --git a/lib/storage_manager_hive.dart b/lib/storage_manager_hive.dart new file mode 100644 index 0000000..2dfa243 --- /dev/null +++ b/lib/storage_manager_hive.dart @@ -0,0 +1,29 @@ +import "package:aptabase_flutter/storage_manager.dart"; + +import "package:hive_flutter/hive_flutter.dart"; + +class HiveStorage implements StorageManager { + late final Box _box; + + @override + Future init() async { + await Hive.initFlutter(); + + _box = await Hive.openBox("aptabase_events"); + } + + @override + Future deleteAllKeys(Iterable keys) { + return _box.deleteAll(keys); + } + + @override + Future>> getItems(int length) async { + return _box.toMap().entries.take(length); + } + + @override + Future add(String item) { + return _box.add(item); + } +} diff --git a/lib/sys_info.dart b/lib/sys_info.dart index 5e5139c..c061fc8 100644 --- a/lib/sys_info.dart +++ b/lib/sys_info.dart @@ -1,17 +1,10 @@ -import 'package:universal_io/io.dart'; - -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; -import 'package:package_info_plus/package_info_plus.dart'; +import "package:device_info_plus/device_info_plus.dart"; +import "package:flutter/foundation.dart"; +import "package:package_info_plus/package_info_plus.dart"; +import "package:universal_io/io.dart"; /// System information about the current device. class SystemInfo { - String osName; - String osVersion; - String appVersion; - String buildNumber; - String locale; - SystemInfo._({ required this.osName, required this.osVersion, @@ -20,91 +13,92 @@ class SystemInfo { required this.appVersion, }); - static const String _kAndroidOsName = 'Android'; - static const String _kIPadOsName = 'iPadOS'; - static const String _kIPhoneOsName = 'iOS'; - static const String _kMacOsName = 'macOS'; - static const String _kWindowsOsName = 'Windows'; - static const String _kWebOsName = ''; - static const String _kWebOsVersion = ''; + String osName; + String osVersion; + String appVersion; + String buildNumber; + String locale; - static const String _kUnknownOsVersion = ''; - static const String _kIpadModelString = 'ipad'; + static const String _kAndroidOsName = "Android"; + static const String _kIPadOsName = "iPadOS"; + static const String _kIPhoneOsName = "iOS"; + static const String _kMacOsName = "macOS"; + static const String _kWindowsOsName = "Windows"; + static const String _kWebOsName = ""; + static const String _kWebOsVersion = ""; - /// Returns the system information for the current device. - static Future get() async { - final deviceInfo = DeviceInfoPlugin(); - final packageInfo = await PackageInfo.fromPlatform(); + static const String _kUnknownOsVersion = ""; + static const String _kIpadModelString = "ipad"; - final osName = await _getOsName(deviceInfo); - if (osName == null) { - return null; - } + /// Returns the system information for the current device. + static Future get() async { + final osInfo = await _getOsInfo(); - final osVersion = await _getOsVersion(deviceInfo); + final packageInfo = await PackageInfo.fromPlatform(); return SystemInfo._( - osName: osName, - osVersion: osVersion, + osName: osInfo.name, + osVersion: osInfo.version, locale: Platform.localeName, buildNumber: packageInfo.buildNumber, appVersion: packageInfo.version, ); } - /// Returns the name of the operating system. - static Future _getOsName(DeviceInfoPlugin deviceInfo) async { + /// Returns info (name and version) of the operating system. + static Future<({String name, String version})> _getOsInfo() async { if (kIsWeb) { - return _kWebOsName; - } else if (Platform.isAndroid) { - return _kAndroidOsName; - } else if (Platform.isIOS) { - final info = await deviceInfo.iosInfo; - final iPad = info.model.toLowerCase().contains(_kIpadModelString); - return iPad ? _kIPadOsName : _kIPhoneOsName; - } else if (Platform.isMacOS) { - return _kMacOsName; - } else if (Platform.isWindows) { - return _kWindowsOsName; - } else if (Platform.isLinux) { - final info = await deviceInfo.linuxInfo; - return info.name; + return (name: _kWebOsName, version: _kWebOsVersion); } - return null; - } - - /// Returns the version of the operating system. - static Future _getOsVersion(DeviceInfoPlugin deviceInfo) async { - if (kIsWeb) { - return _kWebOsVersion; - } + final deviceInfo = DeviceInfoPlugin(); if (Platform.isAndroid) { final info = await deviceInfo.androidInfo; - return info.version.release; + return ( + name: _kAndroidOsName, + version: info.version.release, + ); } if (Platform.isIOS) { final info = await deviceInfo.iosInfo; - return info.systemVersion; + final isIPad = info.model.toLowerCase().contains(_kIpadModelString); + final osName = isIPad ? _kIPadOsName : _kIPhoneOsName; + + return (name: osName, version: info.systemVersion); } if (Platform.isMacOS) { final info = await deviceInfo.macOsInfo; - return '${info.majorVersion}.${info.minorVersion}.${info.patchVersion}'; + final version = "${info.majorVersion}." + "${info.minorVersion}." + "${info.patchVersion}"; + + return (name: _kMacOsName, version: version); } if (Platform.isWindows) { final info = await deviceInfo.windowsInfo; - return '${info.majorVersion}.${info.minorVersion}.${info.buildNumber}'; + final version = "${info.majorVersion}." + "${info.minorVersion}." + "${info.buildNumber}"; + + return (name: _kWindowsOsName, version: version); } if (Platform.isLinux) { final info = await deviceInfo.linuxInfo; - return info.versionId ?? _kUnknownOsVersion; + + return ( + name: info.name, + version: info.versionId ?? _kUnknownOsVersion, + ); } - return _kUnknownOsVersion; + return ( + name: Platform.operatingSystem, + version: Platform.operatingSystemVersion, + ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 938937c..bee9953 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,8 +15,10 @@ dependencies: package_info_plus: ^8.0.0 device_info_plus: ^10.1.0 universal_io: ^2.2.2 + hive: ^2.2.3 + hive_flutter: ^1.1.0 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^4.0.0 \ No newline at end of file + flutter_lints: ^4.0.0