From c307f76d7b12c80dc20939891c00c8059b286536 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Tue, 28 Feb 2023 21:51:17 +0300 Subject: [PATCH 1/3] fix: Add displaying coursework in grades (#298) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Исправлено #297 * Теперь оценки отображаются только для активного профиля ЛКС --- lib/data/datasources/user_remote.dart | 34 +++++++++++++------ .../repositories/user_repository_impl.dart | 3 +- lib/domain/repositories/user_repository.dart | 2 +- lib/domain/usecases/get_scores.dart | 5 +-- .../bloc/scores_bloc/scores_bloc.dart | 10 +++++- .../bloc/scores_bloc/scores_event.dart | 4 ++- .../pages/profile/profile_scores_page.dart | 13 +++++-- 7 files changed, 52 insertions(+), 19 deletions(-) diff --git a/lib/data/datasources/user_remote.dart b/lib/data/datasources/user_remote.dart index 386fa5ef..79c19270 100644 --- a/lib/data/datasources/user_remote.dart +++ b/lib/data/datasources/user_remote.dart @@ -18,7 +18,7 @@ abstract class UserRemoteData { Future> getAnnounces(); Future> getEmployees(String name); Future> getAttendance(String dateStart, String dateEnd); - Future>> getScores(); + Future>>> getScores(); Future> getNfcPasses( String code, String studentId, String deviceId); Future getNfcCode(String code, String studentId, String deviceId); @@ -132,7 +132,7 @@ class UserRemoteDataImpl implements UserRemoteData { } @override - Future>> getScores() async { + Future>>> getScores() async { final response = await lksOauth2.oauth2Helper.get( '$_apiUrl/?action=getData&url=https://lk.mirea.ru/learning/scores/', ); @@ -146,18 +146,30 @@ class UserRemoteDataImpl implements UserRemoteData { } if (response.statusCode == 200) { - Map> scores = {}; - jsonResponse["SCORES"].values.first.forEach((key, value) { - if (scores.containsKey(key) == false) { - scores[key] = []; - } + Map>> scoresByStudentCode = {}; + final scoresRes = jsonResponse["SCORES"] as Map; - for (final score in value.values) { - scores[key]!.add(ScoreModel.fromJson(score[0])); - } + scoresRes.forEach((studentId, semesters) { + Map> scoresBySemester = {}; + final semestersRes = semesters as Map; + + semestersRes.forEach((semester, subjects) { + List scores = []; + final subjectsRes = subjects as Map; + + subjectsRes.forEach((subjectId, score) { + for (final score in score) { + scores.add(ScoreModel.fromJson(score)); + } + }); + + scoresBySemester[semester] = scores; + }); + + scoresByStudentCode[studentId] = scoresBySemester; }); - return scores; + return scoresByStudentCode; } else { throw ServerException('Response status code is ${response.statusCode}'); } diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index c0311676..a385ed01 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -91,7 +91,8 @@ class UserRepositoryImpl implements UserRepository { } @override - Future>>> getScores() async { + Future>>>> + getScores() async { try { final scores = await remoteDataSource.getScores(); return Right(scores); diff --git a/lib/domain/repositories/user_repository.dart b/lib/domain/repositories/user_repository.dart index 486c55cf..9905c831 100644 --- a/lib/domain/repositories/user_repository.dart +++ b/lib/domain/repositories/user_repository.dart @@ -13,7 +13,7 @@ abstract class UserRepository { Future> getUserData(); Future>> getAnnounces(); Future>> getEmployees(String name); - Future>>> getScores(); + Future>>>> getScores(); Future>> getAattendance( String dateStart, String dateEnd); Future> getAuthToken(); diff --git a/lib/domain/usecases/get_scores.dart b/lib/domain/usecases/get_scores.dart index 9030171d..65874b70 100644 --- a/lib/domain/usecases/get_scores.dart +++ b/lib/domain/usecases/get_scores.dart @@ -4,13 +4,14 @@ import 'package:rtu_mirea_app/domain/entities/score.dart'; import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; import 'package:rtu_mirea_app/domain/usecases/usecase.dart'; -class GetScores extends UseCase>, void> { +class GetScores extends UseCase>>, void> { final UserRepository userRepository; GetScores(this.userRepository); @override - Future>>> call([params]) async { + Future>>>> call( + [params]) async { return userRepository.getScores(); } } diff --git a/lib/presentation/bloc/scores_bloc/scores_bloc.dart b/lib/presentation/bloc/scores_bloc/scores_bloc.dart index 28116073..31d7795c 100644 --- a/lib/presentation/bloc/scores_bloc/scores_bloc.dart +++ b/lib/presentation/bloc/scores_bloc/scores_bloc.dart @@ -46,10 +46,18 @@ class ScoresBloc extends Bloc { emit(ScoresLoading()); final scores = await getScores(); + final studentCode = event.studentCode; scores.fold((failure) => emit(ScoresLoadError()), (result) { + final scores = result[studentCode]; + + if (scores == null) { + emit(ScoresLoadError()); + return; + } + emit(ScoresLoaded( - scores: _sortScores(result), selectedSemester: result.keys.last)); + scores: _sortScores(scores), selectedSemester: scores.keys.last)); }); } } diff --git a/lib/presentation/bloc/scores_bloc/scores_event.dart b/lib/presentation/bloc/scores_bloc/scores_event.dart index 8a4ae1d8..19490c6a 100644 --- a/lib/presentation/bloc/scores_bloc/scores_event.dart +++ b/lib/presentation/bloc/scores_bloc/scores_event.dart @@ -8,7 +8,9 @@ abstract class ScoresEvent extends Equatable { } class LoadScores extends ScoresEvent { - const LoadScores(); + const LoadScores({required this.studentCode}); + + final String studentCode; @override List get props => []; diff --git a/lib/presentation/pages/profile/profile_scores_page.dart b/lib/presentation/pages/profile/profile_scores_page.dart index 370134ca..0717cf36 100644 --- a/lib/presentation/pages/profile/profile_scores_page.dart +++ b/lib/presentation/pages/profile/profile_scores_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get/get.dart'; import 'package:rtu_mirea_app/domain/entities/score.dart'; import 'package:rtu_mirea_app/presentation/bloc/scores_bloc/scores_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; @@ -56,10 +57,18 @@ class _ProfileScoresPageState extends State { child: BlocBuilder( builder: (context, userState) { return userState.maybeMap( - logInSuccess: (_) => BlocBuilder( + logInSuccess: (userStateLoaded) => + BlocBuilder( builder: (context, state) { + final user = userStateLoaded.user; + var student = user.students.firstWhereOrNull( + (element) => element.status == 'активный'); + student ??= user.students.first; + if (state is ScoresInitial) { - context.read().add(const LoadScores()); + context + .read() + .add(LoadScores(studentCode: student.code)); } else if (state is ScoresLoaded) { _tabValueNotifier.value = state.scores.keys .toList() From c0fb24186913f0893c281a74a678eb530b406bad Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Mon, 6 Mar 2023 22:16:22 +0300 Subject: [PATCH 2/3] fix: Create new interner connection checker (#300) --- lib/common/utils/connection_checker.dart | 74 +++++++++++++++++++ .../repositories/forum_repository_impl.dart | 4 +- .../repositories/github_repository_impl.dart | 4 +- .../repositories/news_repository_impl.dart | 4 +- .../schedule_repository_impl.dart | 4 +- .../repositories/strapi_repository_impl.dart | 4 +- .../repositories/user_repository_impl.dart | 4 +- lib/service_locator.dart | 4 +- pubspec.yaml | 7 +- 9 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 lib/common/utils/connection_checker.dart diff --git a/lib/common/utils/connection_checker.dart b/lib/common/utils/connection_checker.dart new file mode 100644 index 00000000..95a2905e --- /dev/null +++ b/lib/common/utils/connection_checker.dart @@ -0,0 +1,74 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:connectivity_plus/connectivity_plus.dart'; + +/// The default hosts to ping to check for internet connection. +final List defaultHosts = [ + 'ya.ru', + 'google.com', +]; + +/// A class that checks if the device is connected to the internet. +/// +/// It uses the [Connectivity] plugin to check for internet connection. +/// It also uses the [InternetAddress.lookup] method to ping the default hosts. +/// +/// If the device is connected to the internet, the [connectionChange] stream +/// will emit true. +class InternetConnectionChecker { + static final InternetConnectionChecker _singleton = + InternetConnectionChecker._internal(); + InternetConnectionChecker._internal(); + + static InternetConnectionChecker getInstance() => _singleton; + + bool _hasConnection = false; + + /// The stream that emits whenever the connection status changes. + StreamController connectionChangeController = StreamController.broadcast(); + + final Connectivity _connectivity = Connectivity(); + + void initialize() { + _connectivity.onConnectivityChanged.listen(_connectionChange); + checkConnection(); + } + + Stream get connectionChange => connectionChangeController.stream; + + void dispose() { + connectionChangeController.close(); + } + + void _connectionChange(ConnectivityResult result) { + checkConnection(); + } + + Future checkConnection() async { + bool previousConnection = _hasConnection; + + bool currentConnectionStatus = false; + + for (var host in defaultHosts) { + try { + final result = await InternetAddress.lookup(host); + if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { + currentConnectionStatus = true; + break; + } + } on SocketException catch (_) {} + } + + if (previousConnection != currentConnectionStatus) { + _hasConnection = currentConnectionStatus; + connectionChangeController.add(_hasConnection); + } + + return _hasConnection; + } + + Future get hasConnection async => await checkConnection(); + + bool get lastKnownConnection => _hasConnection; +} diff --git a/lib/data/repositories/forum_repository_impl.dart b/lib/data/repositories/forum_repository_impl.dart index 9aad43fd..98d9ceb3 100644 --- a/lib/data/repositories/forum_repository_impl.dart +++ b/lib/data/repositories/forum_repository_impl.dart @@ -1,14 +1,14 @@ import 'package:dartz/dartz.dart'; -import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; +import 'package:rtu_mirea_app/common/utils/connection_checker.dart'; import 'package:rtu_mirea_app/data/datasources/forum_local.dart'; import 'package:rtu_mirea_app/data/datasources/forum_remote.dart'; import 'package:rtu_mirea_app/domain/entities/forum_member.dart'; import 'package:rtu_mirea_app/domain/repositories/forum_repository.dart'; class ForumRepositoryImpl implements ForumRepository { - final InternetConnectionCheckerPlus connectionChecker; + final InternetConnectionChecker connectionChecker; final ForumRemoteData remoteDataSource; final ForumLocalData localDataSource; diff --git a/lib/data/repositories/github_repository_impl.dart b/lib/data/repositories/github_repository_impl.dart index 5ccaf5da..37a846df 100644 --- a/lib/data/repositories/github_repository_impl.dart +++ b/lib/data/repositories/github_repository_impl.dart @@ -1,7 +1,7 @@ import 'package:dartz/dartz.dart'; -import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; +import 'package:rtu_mirea_app/common/utils/connection_checker.dart'; import 'package:rtu_mirea_app/data/datasources/github_local.dart'; import 'package:rtu_mirea_app/data/datasources/github_remote.dart'; import 'package:rtu_mirea_app/domain/entities/contributor.dart'; @@ -10,7 +10,7 @@ import 'package:rtu_mirea_app/domain/repositories/github_repository.dart'; class GithubRepositoryImpl implements GithubRepository { final GithubRemoteData remoteDataSource; final GithubLocalData localDataSource; - final InternetConnectionCheckerPlus connectionChecker; + final InternetConnectionChecker connectionChecker; GithubRepositoryImpl({ required this.remoteDataSource, diff --git a/lib/data/repositories/news_repository_impl.dart b/lib/data/repositories/news_repository_impl.dart index cf7a09aa..d259558c 100644 --- a/lib/data/repositories/news_repository_impl.dart +++ b/lib/data/repositories/news_repository_impl.dart @@ -1,14 +1,14 @@ import 'package:dartz/dartz.dart'; -import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; +import 'package:rtu_mirea_app/common/utils/connection_checker.dart'; import 'package:rtu_mirea_app/data/datasources/news_remote.dart'; import 'package:rtu_mirea_app/domain/entities/news_item.dart'; import 'package:rtu_mirea_app/domain/repositories/news_repository.dart'; class NewsRepositoryImpl implements NewsRepository { final NewsRemoteData remoteDataSource; - final InternetConnectionCheckerPlus connectionChecker; + final InternetConnectionChecker connectionChecker; NewsRepositoryImpl({ required this.remoteDataSource, diff --git a/lib/data/repositories/schedule_repository_impl.dart b/lib/data/repositories/schedule_repository_impl.dart index 648ec41c..d775cc41 100644 --- a/lib/data/repositories/schedule_repository_impl.dart +++ b/lib/data/repositories/schedule_repository_impl.dart @@ -1,7 +1,7 @@ import 'package:dartz/dartz.dart'; -import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; +import 'package:rtu_mirea_app/common/utils/connection_checker.dart'; import 'package:rtu_mirea_app/data/datasources/schedule_local.dart'; import 'package:rtu_mirea_app/data/datasources/schedule_remote.dart'; import 'package:rtu_mirea_app/data/models/schedule_model.dart'; @@ -13,7 +13,7 @@ import 'package:rtu_mirea_app/domain/repositories/schedule_repository.dart'; class ScheduleRepositoryImpl implements ScheduleRepository { final ScheduleRemoteData remoteDataSource; final ScheduleLocalData localDataSource; - final InternetConnectionCheckerPlus connectionChecker; + final InternetConnectionChecker connectionChecker; ScheduleRepositoryImpl({ required this.remoteDataSource, diff --git a/lib/data/repositories/strapi_repository_impl.dart b/lib/data/repositories/strapi_repository_impl.dart index 6dadc66c..0e5037c8 100644 --- a/lib/data/repositories/strapi_repository_impl.dart +++ b/lib/data/repositories/strapi_repository_impl.dart @@ -1,9 +1,9 @@ import 'package:dartz/dartz.dart'; -import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; +import 'package:rtu_mirea_app/common/utils/connection_checker.dart'; import 'package:rtu_mirea_app/data/datasources/strapi_remote.dart'; import 'package:rtu_mirea_app/domain/entities/story.dart'; import 'package:rtu_mirea_app/domain/entities/update_info.dart'; @@ -11,7 +11,7 @@ import 'package:rtu_mirea_app/domain/repositories/strapi_repository.dart'; class StrapiRepositoryImpl implements StrapiRepository { final StrapiRemoteData remoteDataSource; - final InternetConnectionCheckerPlus connectionChecker; + final InternetConnectionChecker connectionChecker; final PackageInfo packageInfo; StrapiRepositoryImpl({ diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index a385ed01..e4f2fcee 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -1,7 +1,7 @@ -import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:dartz/dartz.dart'; +import 'package:rtu_mirea_app/common/utils/connection_checker.dart'; import 'package:rtu_mirea_app/data/datasources/user_local.dart'; import 'package:rtu_mirea_app/data/datasources/user_remote.dart'; import 'package:rtu_mirea_app/domain/entities/announce.dart'; @@ -15,7 +15,7 @@ import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; class UserRepositoryImpl implements UserRepository { final UserRemoteData remoteDataSource; final UserLocalData localDataSource; - final InternetConnectionCheckerPlus connectionChecker; + final InternetConnectionChecker connectionChecker; UserRepositoryImpl({ required this.remoteDataSource, diff --git a/lib/service_locator.dart b/lib/service_locator.dart index f9f3bc53..adf2418f 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -3,6 +3,7 @@ import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rtu_mirea_app/common/oauth.dart'; +import 'package:rtu_mirea_app/common/utils/connection_checker.dart'; import 'package:rtu_mirea_app/data/datasources/app_settings_local.dart'; import 'package:rtu_mirea_app/data/datasources/forum_local.dart'; import 'package:rtu_mirea_app/data/datasources/forum_remote.dart'; @@ -74,7 +75,6 @@ import 'package:rtu_mirea_app/presentation/bloc/update_info_bloc/update_info_blo import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'data/repositories/schedule_repository_impl.dart'; @@ -248,7 +248,7 @@ Future setup() async { encryptedSharedPreferences: true, )); getIt.registerLazySingleton(() => secureStorage); - getIt.registerLazySingleton(() => InternetConnectionCheckerPlus()); + getIt.registerLazySingleton(() => InternetConnectionChecker.getInstance()); final PackageInfo packageInfo = await PackageInfo.fromPlatform(); getIt.registerLazySingleton(() => packageInfo); getIt.registerLazySingleton(() => LksOauth2()); diff --git a/pubspec.yaml b/pubspec.yaml index 78ced6d1..08e18cb3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,9 +70,10 @@ dependencies: # See https://pub.dev/packages/material_floating_search_bar material_floating_search_bar: ^0.3.6 - # Internet connection checker. - # See https://pub.dev/packages/internet_connection_checker_plus - internet_connection_checker_plus: ^1.0.1 + # Flutter plugin for discovering the state of the network (WiFi & mobile/cellular) + # connectivity on Android and iOS. + # See https://pub.dev/packages/connectivity_plus + connectivity_plus: ^3.0.3 # Functional Programming in Dart # See https://pub.dev/packages/dartz/versions/0.10.0-nullsafety.2 From b665dfdef120caee4cc52b336b1d4f386225cbc2 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Fri, 10 Mar 2023 10:16:34 +0300 Subject: [PATCH 3/3] fix: Change default token storage to helper store (#301) --- lib/data/datasources/user_local.dart | 16 +++++++--------- lib/data/repositories/user_repository_impl.dart | 1 - lib/service_locator.dart | 7 +++++-- pubspec.yaml | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/data/datasources/user_local.dart b/lib/data/datasources/user_local.dart index 3064eca6..20b2d20e 100644 --- a/lib/data/datasources/user_local.dart +++ b/lib/data/datasources/user_local.dart @@ -1,11 +1,12 @@ import 'dart:core'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:oauth2_client/oauth2_helper.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; +import 'package:rtu_mirea_app/common/oauth.dart'; import 'package:shared_preferences/shared_preferences.dart'; abstract class UserLocalData { - Future setTokenToCache(String token); Future getTokenFromCache(); Future removeTokenFromCache(); @@ -17,27 +18,24 @@ abstract class UserLocalData { class UserLocalDataImpl implements UserLocalData { final SharedPreferences sharedPreferences; final FlutterSecureStorage secureStorage; + final OAuth2Helper oauthHelper; UserLocalDataImpl({ required this.sharedPreferences, required this.secureStorage, + required this.oauthHelper, }); - @override - Future setTokenToCache(String token) { - return secureStorage.write(key: 'lks_access_token', value: token); - } - @override Future getTokenFromCache() async { - String? token = await secureStorage.read(key: 'lks_access_token'); + var token = await oauthHelper.getToken(); if (token == null) throw CacheException('Auth token are not set'); - return Future.value(token); + return Future.value(token.accessToken); } @override Future removeTokenFromCache() { - return secureStorage.delete(key: 'lks_access_token'); + return oauthHelper.removeAllTokens(); } @override diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index e4f2fcee..79560cf8 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -27,7 +27,6 @@ class UserRepositoryImpl implements UserRepository { if (await connectionChecker.hasConnection) { try { final authToken = await remoteDataSource.auth(); - await localDataSource.setTokenToCache(authToken); return Right(authToken); } catch (e) { if (e is ServerException) { diff --git a/lib/service_locator.dart b/lib/service_locator.dart index adf2418f..993e3653 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -1,6 +1,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; +import 'package:oauth2_client/oauth2_helper.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rtu_mirea_app/common/oauth.dart'; import 'package:rtu_mirea_app/common/utils/connection_checker.dart'; @@ -212,8 +213,10 @@ Future setup() async { localDataSource: getIt(), )); - getIt.registerLazySingleton(() => - UserLocalDataImpl(sharedPreferences: getIt(), secureStorage: getIt())); + getIt.registerLazySingleton(() => UserLocalDataImpl( + sharedPreferences: getIt(), + secureStorage: getIt(), + oauthHelper: getIt().oauth2Helper)); getIt.registerLazySingleton( () => UserRemoteDataImpl(httpClient: getIt(), lksOauth2: getIt())); getIt.registerLazySingleton( diff --git a/pubspec.yaml b/pubspec.yaml index 08e18cb3..33ab5a4e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ publish_to: 'none' # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.3.1+22 +version: 1.3.2+24 environment: sdk: ">=2.19.0 <3.0.0"