diff --git a/.github/workflows/analyze-format.yml b/.github/workflows/ci.yml similarity index 65% rename from .github/workflows/analyze-format.yml rename to .github/workflows/ci.yml index 66c42bc..8e6bd30 100644 --- a/.github/workflows/analyze-format.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Analyze and format +name: Analyze, format and test on: # Trigger the workflow on pushes to the main and feature branches as well as PRs targeting them. @@ -31,3 +31,20 @@ jobs: - name: Check format run: dart format --output=none --set-exit-if-changed . -l 150 + + test: + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.flutter_version }} + channel: 'stable' + cache: true + + - name: Test + run: flutter test + diff --git a/.gitignore b/.gitignore index ad3c2ca..b1bbdd9 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Hive testing +/test/hive_storage_test diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a9bf442..b9d2152 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - Flutter (1.0.0) - - shared_preferences_foundation (0.0.1): + - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - url_launcher_ios (0.0.1): @@ -8,21 +8,21 @@ PODS: DEPENDENCIES: - Flutter (from `Flutter`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) EXTERNAL SOURCES: Flutter: :path: Flutter - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189 diff --git a/lib/entities/accepted_terms_and_conditions.dart b/lib/entities/accepted_terms_and_conditions.dart new file mode 100644 index 0000000..e6a194f --- /dev/null +++ b/lib/entities/accepted_terms_and_conditions.dart @@ -0,0 +1,16 @@ +import 'package:hive_flutter/hive_flutter.dart'; + +part 'accepted_terms_and_conditions.g.dart'; + +/// Version of the Terms & Conditions accepted by the user. +@HiveType(typeId: 1) +class AcceptedTermsAndConditions { + static const table = "accepted_terms_and_conditions"; + + @HiveField(0) + final String version; + @HiveField(1) + final DateTime acceptedAt; + + AcceptedTermsAndConditions({required this.version, required this.acceptedAt}); +} diff --git a/lib/entities/accepted_terms_and_conditions.g.dart b/lib/entities/accepted_terms_and_conditions.g.dart new file mode 100644 index 0000000..21484a7 --- /dev/null +++ b/lib/entities/accepted_terms_and_conditions.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'accepted_terms_and_conditions.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class AcceptedTermsAndConditionsAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + AcceptedTermsAndConditions read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return AcceptedTermsAndConditions( + version: fields[1] as String, + acceptedAt: fields[2] as DateTime, + ); + } + + @override + void write(BinaryWriter writer, AcceptedTermsAndConditions obj) { + writer + ..writeByte(2) + ..writeByte(1) + ..write(obj.version) + ..writeByte(2) + ..write(obj.acceptedAt); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || other is AcceptedTermsAndConditionsAdapter && runtimeType == other.runtimeType && typeId == other.typeId; +} diff --git a/lib/main.dart b/lib/main.dart index dc68a28..9c8df58 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,15 +1,16 @@ +import 'package:concordium_wallet/repositories/terms_and_conditions_repository.dart'; import 'package:concordium_wallet/screens/routes.dart'; import 'package:concordium_wallet/services/http.dart'; -import 'package:concordium_wallet/services/shared_preferences/service.dart'; +import 'package:concordium_wallet/providers/storage.dart'; import 'package:concordium_wallet/services/wallet_proxy/service.dart'; import 'package:concordium_wallet/state/config.dart'; import 'package:concordium_wallet/state/network.dart'; import 'package:concordium_wallet/state/services.dart'; import 'package:concordium_wallet/state/terms_and_conditions.dart'; import 'package:concordium_wallet/theme.dart'; +import 'package:concordium_wallet/types/future_value.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:shared_preferences/shared_preferences.dart'; void main() { runApp(const App()); @@ -33,13 +34,13 @@ Future loadConfig(HttpService http) async { Future bootstrap() async { const http = HttpService(); final configFuture = loadConfig(http); - final prefsFuture = SharedPreferences.getInstance(); + final storageFuture = StorageProvider.init(); final config = await configFuture; - final prefs = await prefsFuture; + final storageService = await storageFuture; return ServiceRepository( config: config, http: http, - sharedPreferences: SharedPreferencesService(prefs), + storage: storageService, ); } @@ -53,21 +54,11 @@ class App extends StatelessWidget { return _WithServiceRepository( child: _WithSelectedNetwork( initialNetwork: initialNetwork, - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) { - // Initialize T&C by loading the currently accepted version from shared preferences. - final prefs = context.read().sharedPreferences; - return TermsAndConditionAcceptance(prefs); - }, - ), - ], - child: MaterialApp( - routes: appRoutes, - theme: concordiumTheme(), - ), - ), + child: _WithTermsAndConditionAcceptance( + child: MaterialApp( + routes: appRoutes, + theme: concordiumTheme(), + )), ), ); } @@ -158,6 +149,48 @@ class _WithSelectedNetworkState extends State<_WithSelectedNetwork> { } } +class _WithTermsAndConditionAcceptance extends StatefulWidget { + final Widget child; + + const _WithTermsAndConditionAcceptance({required this.child}); + + @override + State<_WithTermsAndConditionAcceptance> createState() => _WithTermsAndConditionAcceptanceState(); +} + +class _WithTermsAndConditionAcceptanceState extends State<_WithTermsAndConditionAcceptance> { + late final Future> _lastAccepted; + late final TermsAndConditionsRepository _repository; + + @override + void initState() { + super.initState(); + final storage = context.read().storage; + setState(() { + _repository = TermsAndConditionsRepository(storageProvider: storage); + _lastAccepted = _repository.getAcceptedTermsAndConditions().then(FutureValue.new); + }); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _lastAccepted, + builder: (_, snapshot) { + if (snapshot.data != null) { + return BlocProvider( + create: (_) { + return TermsAndConditionAcceptance(_repository, snapshot.requireData.value); + }, + child: widget.child); + } else if (snapshot.hasError) { + // TODO Handle error + } + return const _Initializing(); + }); + } +} + class _Initializing extends StatelessWidget { const _Initializing(); diff --git a/lib/providers/storage.dart b/lib/providers/storage.dart new file mode 100644 index 0000000..36cac0b --- /dev/null +++ b/lib/providers/storage.dart @@ -0,0 +1,53 @@ +import 'package:concordium_wallet/entities/accepted_terms_and_conditions.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +/// Service for interacting with [Hive]. +class StorageProvider { + final LazyBox _acceptedTermsAndConditionBox; + + const StorageProvider._(this._acceptedTermsAndConditionBox); + + static Future init() async { + await Hive.initFlutter(); + _registerAdapters(); + await _openBoxes(); + + return StorageProvider._(Hive.lazyBox(AcceptedTermsAndConditions.table)); + } + + /// Register all adapters needed for typed boxes. + static void _registerAdapters() { + Hive.registerAdapter(AcceptedTermsAndConditionsAdapter()); + Hive.registerAdapter(PreciseDateTimeAdapter(), override: true, internal: true); + } + + /// Opens all boxes asynchronously. + static Future _openBoxes() async { + final atcFuture = Hive.openLazyBox(AcceptedTermsAndConditions.table); + await Future.wait([atcFuture]); + } + + LazyBox get acceptedTermsAndConditionBox => _acceptedTermsAndConditionBox; +} + +/// A bit modified DateTimeWithTimezoneAdapter (https://github.com/isar/hive/blob/470473ffc1ba39f6c90f31ababe0ee63b76b69fe/hive/lib/src/adapters/date_time_adapter.dart#L25) +/// This adapter is relevant because by default, [Hive] only stores datetimes down to millisecond precision. +/// It's derived from issue in link and proposed as a solution (https://github.com/isar/hive/issues/474#issuecomment-730562545). +/// The type ID needs to be 18 as it's required to overwrite the existing one registered. (https://github.com/isar/hive/blob/470473ffc1ba39f6c90f31ababe0ee63b76b69fe/hive/lib/src/adapters/date_time_adapter.dart#L28). +class PreciseDateTimeAdapter extends TypeAdapter { + @override + final typeId = 18; + + @override + DateTime read(BinaryReader reader) { + var micros = reader.readInt(); + var isUtc = reader.readBool(); + return DateTime.fromMicrosecondsSinceEpoch(micros, isUtc: isUtc); + } + + @override + void write(BinaryWriter writer, DateTime obj) { + writer.writeInt(obj.microsecondsSinceEpoch); + writer.writeBool(obj.isUtc); + } +} diff --git a/lib/repositories/terms_and_conditions_repository.dart b/lib/repositories/terms_and_conditions_repository.dart new file mode 100644 index 0000000..0a78b08 --- /dev/null +++ b/lib/repositories/terms_and_conditions_repository.dart @@ -0,0 +1,38 @@ +import 'package:concordium_wallet/providers/storage.dart'; +import 'package:concordium_wallet/state/terms_and_conditions.dart'; +import 'package:concordium_wallet/entities/accepted_terms_and_conditions.dart'; + +class TermsAndConditionsRepository { + static const String key = "accepted_terms_and_condition"; + + final StorageProvider _storageProvider; + + const TermsAndConditionsRepository({required StorageProvider storageProvider}) : _storageProvider = storageProvider; + + /// Reads the currently accepted T&C version. + Future getAcceptedTermsAndConditions() async { + var model = await _storageProvider.acceptedTermsAndConditionBox.get(key); + return _toState(model); + } + + /// Writes the currently accepted T&C version. + Future writeAcceptedTermsAndConditions(AcceptedTermsAndConditionsState acceptedTermsAndConditions) { + return _storageProvider.acceptedTermsAndConditionBox.put(key, _fromState(acceptedTermsAndConditions)); + } + + /// Deletes the currently accepted T&C version. + Future deleteTermsAndConditionsAcceptedVersion() { + return _storageProvider.acceptedTermsAndConditionBox.delete(key); + } + + AcceptedTermsAndConditions _fromState(AcceptedTermsAndConditionsState state) { + return AcceptedTermsAndConditions(version: state.version, acceptedAt: state.acceptedAt); + } + + AcceptedTermsAndConditionsState? _toState(AcceptedTermsAndConditions? model) { + if (model == null) { + return null; + } + return AcceptedTermsAndConditionsState(version: model.version, acceptedAt: model.acceptedAt); + } +} diff --git a/lib/screens/home/screen.dart b/lib/screens/home/screen.dart index 14bd6ec..5da0b89 100644 --- a/lib/screens/home/screen.dart +++ b/lib/screens/home/screen.dart @@ -92,7 +92,7 @@ class _HomeScreenState extends State { const SizedBox(height: 8), ElevatedButton( onPressed: () { - const tac = AcceptedTermsAndConditions(version: '1.2.3'); + final tac = AcceptedTermsAndConditionsState.acceptNow('1.2.3'); context.read().userAccepted(tac); }, child: const Text('Set accepted T&C version to 1.2.3'), diff --git a/lib/screens/terms_and_conditions/screen.dart b/lib/screens/terms_and_conditions/screen.dart index 1b4367a..0b034dc 100644 --- a/lib/screens/terms_and_conditions/screen.dart +++ b/lib/screens/terms_and_conditions/screen.dart @@ -124,9 +124,7 @@ class _TermsAndConditionsScreenState extends State { Function()? _onAcceptButtonPressed(BuildContext context) { if (isAccepted) { return () { - final tac = AcceptedTermsAndConditions( - version: widget.validTermsAndConditions.version, - ); + final tac = AcceptedTermsAndConditionsState.acceptNow(widget.validTermsAndConditions.version); context.read().userAccepted(tac); }; } diff --git a/lib/services/shared_preferences/service.dart b/lib/services/shared_preferences/service.dart deleted file mode 100644 index cb18c14..0000000 --- a/lib/services/shared_preferences/service.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; - -/// Service for interacting with [SharedPreferences]. -class SharedPreferencesService { - /// String key associated with the persisted accepted T&C version. - static const _tacAcceptedVersionKey = 'tac:accepted_version'; - - /// Wrapped instance. - final SharedPreferences _prefs; - - const SharedPreferencesService(this._prefs); - - /// Reads the currently accepted T&C version. - String? get termsAndConditionsAcceptedVersion => _prefs.getString(_tacAcceptedVersionKey); - - /// Writes the currently accepted T&C version. - Future writeTermsAndConditionsAcceptedVersion(String version) async { - await _prefs.setString(_tacAcceptedVersionKey, version); - } - - /// Deletes the currently accepted T&C version. - Future deleteTermsAndConditionsAcceptedVersion() async { - await _prefs.remove(_tacAcceptedVersionKey); - } -} diff --git a/lib/state/services.dart b/lib/state/services.dart index 8b1c044..b80c18b 100644 --- a/lib/state/services.dart +++ b/lib/state/services.dart @@ -1,5 +1,5 @@ import 'package:concordium_wallet/services/http.dart'; -import 'package:concordium_wallet/services/shared_preferences/service.dart'; +import 'package:concordium_wallet/providers/storage.dart'; import 'package:concordium_wallet/services/wallet_proxy/service.dart'; import 'package:concordium_wallet/state/config.dart'; import 'package:concordium_wallet/state/network.dart'; @@ -36,10 +36,10 @@ class ServiceRepository { /// Global service for performing HTTP calls. final HttpService http; - /// Global service for interacting with shared preferences. - final SharedPreferencesService sharedPreferences; + /// Global service for interacting with storage. + final StorageProvider storage; - ServiceRepository({required this.config, required this.http, required this.sharedPreferences}); + ServiceRepository({required this.config, required this.http, required this.storage}); /// Activate the network with the provided name. /// diff --git a/lib/state/terms_and_conditions.dart b/lib/state/terms_and_conditions.dart index 699c9ef..bd303a7 100644 --- a/lib/state/terms_and_conditions.dart +++ b/lib/state/terms_and_conditions.dart @@ -1,13 +1,18 @@ -import 'package:concordium_wallet/services/shared_preferences/service.dart'; +import 'package:concordium_wallet/repositories/terms_and_conditions_repository.dart'; import 'package:concordium_wallet/services/wallet_proxy/model.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Version of the Terms & Conditions accepted by the user. -class AcceptedTermsAndConditions { +class AcceptedTermsAndConditionsState { /// Accepted version. final String version; + final DateTime acceptedAt; - const AcceptedTermsAndConditions({required this.version}); + const AcceptedTermsAndConditionsState({required this.version, required this.acceptedAt}); + + factory AcceptedTermsAndConditionsState.acceptNow(String acceptedVersion) { + return AcceptedTermsAndConditionsState(version: acceptedVersion, acceptedAt: DateTime.now()); + } /// Whether the accepted version is valid with respect to the provided valid version. bool isValid(TermsAndConditions tac) { @@ -37,8 +42,8 @@ class ValidTermsAndConditions { class TermsAndConditionsAcceptanceState { /// Currently accepted T&C. /// - /// The accepted version is persisted into shared preferences. - final AcceptedTermsAndConditions? accepted; + /// The accepted version persisted. + final AcceptedTermsAndConditionsState? accepted; /// Currently valid T&C. final ValidTermsAndConditions? valid; @@ -49,19 +54,19 @@ class TermsAndConditionsAcceptanceState { /// State component of the currently accepted and valid Terms & Conditions. class TermsAndConditionAcceptance extends Cubit { /// Service used to persist the accepted T&C version. - final SharedPreferencesService _prefs; + final TermsAndConditionsRepository _termsAndConditionRepo; - TermsAndConditionAcceptance(this._prefs) : super(const TermsAndConditionsAcceptanceState(accepted: null, valid: null)) { - final acceptedVersion = _prefs.termsAndConditionsAcceptedVersion; + TermsAndConditionAcceptance(this._termsAndConditionRepo, AcceptedTermsAndConditionsState? acceptedVersion) + : super(const TermsAndConditionsAcceptanceState(accepted: null, valid: null)) { if (acceptedVersion != null) { - userAccepted(AcceptedTermsAndConditions(version: acceptedVersion)); + userAccepted(acceptedVersion); } } /// Update the currently accepted T&C and persist the new value. /// /// Use [resetAccepted] to revoke acceptance. - void userAccepted(AcceptedTermsAndConditions tac) { + void userAccepted(AcceptedTermsAndConditionsState tac) { emit(TermsAndConditionsAcceptanceState(accepted: tac, valid: state.valid)); } @@ -101,17 +106,16 @@ class TermsAndConditionAcceptance extends Cubit change) { super.onChange(change); - // TODO: Pass success/failure status to notification service. - _persistAcceptedVersionIfChanged(change.nextState.accepted?.version, change.currentState.accepted?.version); + if (change.currentState != change.nextState) { + // TODO: Pass success/failure status to notification service. + _persistAcceptedVersion(change.nextState.accepted); + } } - Future _persistAcceptedVersionIfChanged(String? nextAcceptedVersion, String? currentAcceptedVersion) { - if (nextAcceptedVersion == currentAcceptedVersion) { - return Future.value(); - } + Future _persistAcceptedVersion(AcceptedTermsAndConditionsState? nextAcceptedVersion) { if (nextAcceptedVersion == null) { - return _prefs.deleteTermsAndConditionsAcceptedVersion(); + return _termsAndConditionRepo.deleteTermsAndConditionsAcceptedVersion(); } - return _prefs.writeTermsAndConditionsAcceptedVersion(nextAcceptedVersion); + return _termsAndConditionRepo.writeAcceptedTermsAndConditions(nextAcceptedVersion); } } diff --git a/lib/types/future_value.dart b/lib/types/future_value.dart new file mode 100644 index 0000000..0ed1d89 --- /dev/null +++ b/lib/types/future_value.dart @@ -0,0 +1,9 @@ +import 'package:flutter/widgets.dart'; + +/// Wrapper around a nullable return type in a future. +/// By using this one can use [AsyncSnapshot.hasData] in [FutureBuilder]. +class FutureValue { + T? value; + + FutureValue(this.value); +} diff --git a/pubspec.lock b/pubspec.lock index 03a8e77..6b397d4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "64.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.2.0" args: dependency: transitive description: @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: built_value - sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74 + sha256: "69acb7007eb2a31dc901512bfe0f7b767168be34cb734835d54c070bfa74c1b2" url: "https://pub.dev" source: hosted - version: "8.6.3" + version: "8.8.0" characters: dependency: transitive description: @@ -149,18 +149,18 @@ packages: dependency: transitive description: name: code_builder - sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677" + sha256: b2151ce26a06171005b379ecff6e08d34c470180ffe16b8e14b6d52be292b55f url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.8.0" collection: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: coverage - sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" + sha256: ac86d3abab0f165e4b8f561280ff4e066bceaac83c424dd19f1ae2c2fcd12ca9 url: "https://pub.dev" source: hosted - version: "1.6.4" + version: "1.7.1" crypto: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" diff_match_patch: dependency: transitive description: @@ -229,10 +229,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: @@ -266,10 +266,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.9" flutter_test: dependency: "direct dev" description: flutter @@ -304,14 +304,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" http: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" http_multi_server: dependency: transitive description: @@ -396,10 +420,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -456,6 +480,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" + source: hosted + version: "2.3.1" path_provider_linux: dependency: transitive description: @@ -484,10 +532,10 @@ packages: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.1" platform: dependency: transitive description: @@ -500,10 +548,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" pool: dependency: transitive description: @@ -516,10 +564,10 @@ packages: dependency: transitive description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.1" pub_semver: dependency: transitive description: @@ -536,62 +584,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac - url: "https://pub.dev" - source: hosted - version: "2.2.1" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" - url: "https://pub.dev" - source: hosted - version: "2.2.1" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" - url: "https://pub.dev" - source: hosted - version: "2.3.4" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a - url: "https://pub.dev" - source: hosted - version: "2.3.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a - url: "https://pub.dev" - source: hosted - version: "2.3.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf - url: "https://pub.dev" - source: hosted - version: "2.2.1" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f - url: "https://pub.dev" - source: hosted - version: "2.3.1" shelf: dependency: transitive description: @@ -673,18 +665,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -713,26 +705,26 @@ packages: dependency: transitive description: name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.3" + version: "1.24.9" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" test_core: dependency: transitive description: name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.9" timing: dependency: transitive description: @@ -753,90 +745,90 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.2.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.2.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" + sha256: "138bd45b3a456dcfafc46d1a146787424f8d2edfbf2809c9324361e58f851cf7" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.2.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.0" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" + sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.9+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" + sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.9+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" + sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.9+1" vector_math: dependency: transitive description: @@ -849,10 +841,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" watcher: dependency: transitive description: @@ -865,10 +857,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -889,10 +881,10 @@ packages: dependency: transitive description: name: win32 - sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f" url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.1.0" xdg_directories: dependency: transitive description: @@ -905,10 +897,10 @@ packages: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.4.2" yaml: dependency: transitive description: @@ -918,5 +910,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.3 <4.0.0" - flutter: ">=3.13.0" + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index dffe9e2..f1f370f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,9 +39,10 @@ dependencies: build_runner: ^2.4.6 flutter_svg: ^2.0.7 url_launcher: ^6.1.14 - shared_preferences: ^2.2.1 http: ^1.1.0 flutter_bloc: ^8.1.3 + hive: ^2.2.3 + hive_flutter: ^1.1.0 dev_dependencies: flutter_test: @@ -54,6 +55,7 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^2.0.0 json_serializable: ^6.7.1 + hive_generator: ^2.0.1 mocktail: ^1.0.1 bloc_test: ^9.1.5 diff --git a/test/repositories/terms_and_conditions_repository_test.dart b/test/repositories/terms_and_conditions_repository_test.dart new file mode 100644 index 0000000..aff0ce2 --- /dev/null +++ b/test/repositories/terms_and_conditions_repository_test.dart @@ -0,0 +1,78 @@ +import 'package:concordium_wallet/entities/accepted_terms_and_conditions.dart'; +import 'package:concordium_wallet/providers/storage.dart'; +import 'package:concordium_wallet/repositories/terms_and_conditions_repository.dart'; +import 'package:concordium_wallet/state/terms_and_conditions.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; + +void main() { + /// Mock getApplicationDocumentsDirectory on channel plugins.flutter.io/path_provider + mockPathProvider() { + TestWidgetsFlutterBinding.ensureInitialized(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(const MethodChannel("plugins.flutter.io/path_provider"), (MethodCall methodCall) async { + return "./test/hive_storage_test"; + }); + } + + late TermsAndConditionsRepository repository; + + setUpAll(() async { + mockPathProvider(); + final storage = await StorageProvider.init(); + repository = TermsAndConditionsRepository(storageProvider: storage); + }); + + tearDownAll(() => Hive.deleteFromDisk()); + + tearDown(() => Hive.lazyBox(AcceptedTermsAndConditions.table).clear()); + + test('When add accepted terms and condition to storage, then saved', () async { + // Arrange + const expectedVersion = "0.0.42"; + final accepted = AcceptedTermsAndConditionsState.acceptNow(expectedVersion); + + // Act + await repository.writeAcceptedTermsAndConditions(accepted); + + // Assert + final actual = await repository.getAcceptedTermsAndConditions(); + expect(actual, isNotNull); + expect(actual!.version, expectedVersion); + }); + + test("When delete accepted terms and condition from storage, then empty", () async { + // Arrange + const expectedVersion = "0.0.42"; + final accepted = AcceptedTermsAndConditionsState.acceptNow(expectedVersion); + await repository.writeAcceptedTermsAndConditions(accepted); + expect(await Hive.lazyBox(AcceptedTermsAndConditions.table).get(TermsAndConditionsRepository.key), isNotNull); + + // Act + await repository.deleteTermsAndConditionsAcceptedVersion(); + + // Assert + final actual = await repository.getAcceptedTermsAndConditions(); + expect(actual, null); + }); + + test("When update accepted terms and conditions, then version updated", () async { + // Arrange + const oldVersion = "0.0.42"; + const newVersion = "0.0.84"; + final oldAccepted = AcceptedTermsAndConditionsState.acceptNow(oldVersion); + final newAccepted = AcceptedTermsAndConditionsState.acceptNow(newVersion); + await repository.writeAcceptedTermsAndConditions(oldAccepted); + expect(await Hive.lazyBox(AcceptedTermsAndConditions.table).get(TermsAndConditionsRepository.key), isNotNull); + + // Act + await repository.writeAcceptedTermsAndConditions(newAccepted); + + // Assert + final actual = await repository.getAcceptedTermsAndConditions(); + expect(actual, isNotNull); + expect(actual!.acceptedAt, newAccepted.acceptedAt); + expect(actual.version, newAccepted.version); + }); +} diff --git a/test/terms_and_conditions_test.dart b/test/terms_and_conditions_test.dart index b8edcb2..b3eb77f 100644 --- a/test/terms_and_conditions_test.dart +++ b/test/terms_and_conditions_test.dart @@ -25,7 +25,7 @@ void main() { const String acceptedVersion = "1.0.0"; setUpAll(() { - registerFallbackValue(const AcceptedTermsAndConditions(version: validVersion)); + registerFallbackValue(AcceptedTermsAndConditionsState.acceptNow(validVersion)); }); setUp(() { @@ -33,7 +33,7 @@ void main() { final terms = TermsAndConditions(Uri.parse("localhost"), validVersion); state = TermsAndConditionsAcceptanceState( - accepted: const AcceptedTermsAndConditions(version: acceptedVersion), + accepted: AcceptedTermsAndConditionsState.acceptNow(acceptedVersion), valid: ValidTermsAndConditions.refreshedNow(termsAndConditions: terms)); // Build the terms and condition screen we wish to test