feat: Add login integration test (WIP), update notes feature

This commit is contained in:
Anton Stubenbord
2024-01-06 19:23:30 +01:00
parent 64d49a4a24
commit 497777c52b
20 changed files with 465 additions and 418 deletions

View File

@@ -0,0 +1,2 @@
- New feature: Notes
- Several bugfixes

16
build.yaml Normal file
View File

@@ -0,0 +1,16 @@
targets:
$default:
include:
- pubspec.yaml
sources:
- assets/**
- lib/$lib$
- lib/**.dart
- test/**.dart
- integration_test/**.dart
builders:
mockito|mockBuilder:
generate_for:
- test/**.dart
- integration_test/**.dart

View File

@@ -1,224 +1,130 @@
// import 'package:flutter/material.dart'; import 'dart:io';
// import 'package:flutter_test/flutter_test.dart';
// import 'package:mockito/mockito.dart';
// import 'package:paperless_api/paperless_api.dart';
// import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
// import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
// import 'package:paperless_mobile/core/store/local_vault.dart';
// import 'package:paperless_mobile/di_test_mocks.mocks.dart';
// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
// import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
// import 'package:paperless_mobile/features/settings/model/view_type.dart';
// import 'src/framework.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive/hive.dart';
import 'package:integration_test/integration_test.dart';
import 'package:mockito/mockito.dart';
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
import 'package:paperless_mobile/core/database/hive/hive_initialization.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/keys.dart';
import 'package:paperless_mobile/main.dart'
show initializeDefaultParameters, AppEntrypoint;
import 'package:path_provider/path_provider.dart';
// void main() async { import 'src/mocks/mock_paperless_api.dart';
// final t = await initializeTestingFramework(languageCode: 'de');
// const testServerUrl = 'https://example.com'; class MockConnectivityStatusService extends Mock
// const testUsername = 'user'; implements ConnectivityStatusService {}
// const testPassword = 'pass';
// final serverAddressField = find.byKey(const ValueKey('login-server-address')); class MockLocalAuthService extends Mock implements LocalAuthenticationService {}
// final usernameField = find.byKey(const ValueKey('login-username'));
// final passwordField = find.byKey(const ValueKey('login-password'));
// final loginBtn = find.byKey(const ValueKey('login-login-button'));
// testWidgets('Test successful login flow', (WidgetTester tester) async { class MockSessionManager extends Mock implements SessionManager {}
// await initAndLaunchTestApp(tester, () async {
// // Initialize dat for mocked classes
// when((getIt<ConnectivityStatusService>()).connectivityChanges())
// .thenAnswer((i) => Stream.value(true));
// when((getIt<LocalVault>() as MockLocalVault)
// .loadAuthenticationInformation())
// .thenAnswer((realInvocation) async => null);
// when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
// preferredLocaleSubtag: 'en',
// preferredThemeMode: ThemeMode.light,
// isLocalAuthenticationEnabled: false,
// preferredViewType: ViewType.list,
// showInboxOnStartup: false,
// ));
// when(getIt<PaperlessAuthenticationApi>().login(
// username: testUsername,
// password: testPassword,
// )).thenAnswer((i) => Future.value("eyTestToken"));
// await getIt<ConnectivityCubit>().initialize(); class MockLocalNotificationService extends Mock
// await getIt<ApplicationSettingsCubit>().initialize(); implements LocalNotificationService {}
// });
// // Mocked classes void main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const locale = Locale("en", "US");
const testServerUrl = 'https://example.com';
const testUsername = 'user';
const testPassword = 'pass';
// await t.binding.waitUntilFirstFrameRasterized; final hiveDirectory = await getTemporaryDirectory();
// await tester.pumpAndSettle();
// await tester.enterText(serverAddressField, testServerUrl); late ConnectivityStatusService connectivityStatusService;
// await tester.pumpAndSettle(); late MockPaperlessApiFactory paperlessApiFactory;
late AuthenticationCubit authenticationCubit;
late LocalNotificationService localNotificationService;
late SessionManager sessionManager;
final localAuthService = MockLocalAuthService();
// await tester.enterText(usernameField, testUsername); setUp(() async {
// await tester.pumpAndSettle(); connectivityStatusService = MockConnectivityStatusService();
paperlessApiFactory = MockPaperlessApiFactory();
sessionManager = MockSessionManager();
localNotificationService = MockLocalNotificationService();
// await tester.enterText(passwordField, testPassword); authenticationCubit = AuthenticationCubit(
localAuthService,
paperlessApiFactory,
sessionManager,
connectivityStatusService,
localNotificationService,
);
await initHive(
hiveDirectory,
locale.toString(),
);
});
testWidgets(
'A user shall be successfully logged in when providing correct credentials.',
(tester) async {
// Reset data to initial state with given [locale].
await Hive.globalSettingsBox.setValue(
GlobalSettings(
preferredLocaleSubtag: locale.toString(),
loggedInUserId: null,
),
);
when(paperlessApiFactory.authenticationApi.login(
username: testUsername,
password: testPassword,
)).thenAnswer((_) async => "token");
// FocusManager.instance.primaryFocus?.unfocus(); await initializeDefaultParameters();
// await tester.pumpAndSettle();
// await tester.tap(loginBtn); await tester.pumpWidget(
AppEntrypoint(
apiFactory: paperlessApiFactory,
authenticationCubit: authenticationCubit,
connectivityStatusService: connectivityStatusService,
localNotificationService: localNotificationService,
localAuthService: localAuthService,
sessionManager: sessionManager,
),
);
await tester.binding.waitUntilFirstFrameRasterized;
await tester.pumpAndSettle();
// verify(getIt<PaperlessAuthenticationApi>().login( await tester.enterText(
// username: testUsername, find.byKey(TestKeys.login.serverAddressFormField),
// password: testPassword, testServerUrl,
// )).called(1); );
// }); await tester.pumpAndSettle();
// testWidgets('Test login validation missing password', await tester.press(find.byKey(TestKeys.login.continueButton));
// (WidgetTester tester) async {
// await initAndLaunchTestApp(tester, () async {
// when((getIt<ConnectivityStatusService>() as MockConnectivityStatusService)
// .connectivityChanges())
// .thenAnswer((i) => Stream.value(true));
// when((getIt<LocalVault>() as MockLocalVault)
// .loadAuthenticationInformation())
// .thenAnswer((realInvocation) async => null);
// when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings()) await tester.pumpAndSettle();
// .thenAnswer((realInvocation) async => ApplicationSettingsState( expect(
// preferredLocaleSubtag: 'en', find.byKey(TestKeys.login.usernameFormField),
// preferredThemeMode: ThemeMode.light, findsOneWidget,
// isLocalAuthenticationEnabled: false, );
// preferredViewType: ViewType.list,
// showInboxOnStartup: false,
// ));
// await getIt<ConnectivityCubit>().initialize(); await tester.enterText(
// await getIt<ApplicationSettingsCubit>().initialize(); find.byKey(TestKeys.login.usernameFormField),
// }); testUsername,
// // Mocked classes );
await tester.enterText(
find.byKey(TestKeys.login.passwordFormField),
testUsername,
);
await tester.pumpAndSettle();
// // Initialize dat for mocked classes await tester.press(find.byKey(TestKeys.login.loginButton));
await tester.pumpAndSettle();
// await t.binding.waitUntilFirstFrameRasterized; expect(
// await tester.pumpAndSettle(); find.byKey(TestKeys.login.loggingInScreen),
findsOneWidget,
// await tester.enterText(serverAddressField, testServerUrl); );
// await tester.pumpAndSettle(); });
}
// await tester.enterText(usernameField, testUsername);
// await tester.pumpAndSettle();
// FocusManager.instance.primaryFocus?.unfocus();
// await tester.pumpAndSettle();
// await tester.tap(loginBtn);
// await tester.pumpAndSettle();
// verifyNever(
// (getIt<PaperlessAuthenticationApi>() as MockPaperlessAuthenticationApi)
// .login(
// username: testUsername,
// password: testPassword,
// ));
// expect(
// find.textContaining(t.translations.passwordMustNotBeEmpty),
// findsOneWidget,
// );
// });
// testWidgets('Test login validation missing username',
// (WidgetTester tester) async {
// await initAndLaunchTestApp(tester, () async {
// when((getIt<ConnectivityStatusService>() as MockConnectivityStatusService)
// .connectivityChanges())
// .thenAnswer((i) => Stream.value(true));
// when((getIt<LocalVault>() as MockLocalVault)
// .loadAuthenticationInformation())
// .thenAnswer((realInvocation) async => null);
// when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
// preferredLocaleSubtag: 'en',
// preferredThemeMode: ThemeMode.light,
// isLocalAuthenticationEnabled: false,
// preferredViewType: ViewType.list,
// showInboxOnStartup: false,
// ));
// await getIt<ConnectivityCubit>().initialize();
// await getIt<ApplicationSettingsCubit>().initialize();
// });
// await t.binding.waitUntilFirstFrameRasterized;
// await tester.pumpAndSettle();
// await tester.enterText(serverAddressField, testServerUrl);
// await tester.pumpAndSettle();
// await tester.enterText(passwordField, testPassword);
// await tester.pumpAndSettle();
// FocusManager.instance.primaryFocus?.unfocus();
// await tester.pumpAndSettle();
// await tester.tap(loginBtn);
// await tester.pumpAndSettle();
// verifyNever(
// (getIt<PaperlessAuthenticationApi>() as MockPaperlessAuthenticationApi)
// .login(
// username: testUsername,
// password: testPassword,
// ));
// expect(
// find.textContaining(t.translations.usernameMustNotBeEmpty),
// findsOneWidget,
// );
// });
// testWidgets('Test login validation missing server address',
// (WidgetTester tester) async {
// initAndLaunchTestApp(tester, () async {
// when((getIt<ConnectivityStatusService>()).connectivityChanges())
// .thenAnswer((i) => Stream.value(true));
// when((getIt<LocalVault>()).loadAuthenticationInformation())
// .thenAnswer((realInvocation) async => null);
// when((getIt<LocalVault>()).loadApplicationSettings())
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
// preferredLocaleSubtag: 'en',
// preferredThemeMode: ThemeMode.light,
// isLocalAuthenticationEnabled: false,
// preferredViewType: ViewType.list,
// showInboxOnStartup: false,
// ));
// await getIt<ConnectivityCubit>().initialize();
// await getIt<ApplicationSettingsCubit>().initialize();
// });
// await t.binding.waitUntilFirstFrameRasterized;
// await tester.pumpAndSettle();
// await tester.enterText(usernameField, testUsername);
// await tester.pumpAndSettle();
// await tester.enterText(passwordField, testPassword);
// await tester.pumpAndSettle();
// FocusManager.instance.primaryFocus?.unfocus();
// await tester.pumpAndSettle();
// await tester.tap(loginBtn);
// await tester.pumpAndSettle();
// verifyNever(getIt<PaperlessAuthenticationApi>().login(
// username: testUsername,
// password: testPassword,
// ));
// expect(
// find.textContaining(
// t.translations.loginPageServerUrlValidatorMessageText),
// findsOneWidget,
// );
// });
// }

View File

@@ -1,7 +1,16 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/main.dart'
show initializeDefaultParameters, AppEntrypoint;
import 'package:path_provider/path_provider.dart';
Future<TestingFrameworkVariables> initializeTestingFramework( Future<TestingFrameworkVariables> initializeTestingFramework(
{String languageCode = 'en'}) async { {String languageCode = 'en'}) async {
@@ -26,11 +35,3 @@ class TestingFrameworkVariables {
required this.translations, required this.translations,
}); });
} }
Future<void> initAndLaunchTestApp(
WidgetTester tester,
Future<void> Function() initializationCallback,
) async {
await initializationCallback();
//runApp(const PaperlessMobileEntrypoint(authenticationCubit: ),));
}

View File

@@ -0,0 +1,65 @@
import 'package:dio/src/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:mockito/annotations.dart';
@GenerateNiceMocks([
MockSpec<PaperlessAuthenticationApi>(),
MockSpec<PaperlessDocumentsApi>(),
MockSpec<PaperlessLabelsApi>(),
MockSpec<PaperlessUserApi>(),
MockSpec<PaperlessServerStatsApi>(),
MockSpec<PaperlessSavedViewsApi>(),
MockSpec<PaperlessTasksApi>(),
])
import 'mock_paperless_api.mocks.dart';
class MockPaperlessApiFactory implements PaperlessApiFactory {
final PaperlessAuthenticationApi authenticationApi =
MockPaperlessAuthenticationApi();
final PaperlessDocumentsApi documentApi = MockPaperlessDocumentsApi();
final PaperlessLabelsApi labelsApi = MockPaperlessLabelsApi();
final PaperlessUserApi userApi = MockPaperlessUserApi();
final PaperlessSavedViewsApi savedViewsApi = MockPaperlessSavedViewsApi();
final PaperlessServerStatsApi serverStatsApi = MockPaperlessServerStatsApi();
final PaperlessTasksApi tasksApi = MockPaperlessTasksApi();
@override
PaperlessAuthenticationApi createAuthenticationApi(Dio dio) {
return authenticationApi;
}
@override
PaperlessDocumentsApi createDocumentsApi(Dio dio, {required int apiVersion}) {
return documentApi;
}
@override
PaperlessLabelsApi createLabelsApi(Dio dio, {required int apiVersion}) {
return labelsApi;
}
@override
PaperlessSavedViewsApi createSavedViewsApi(
Dio dio, {
required int apiVersion,
}) {
return savedViewsApi;
}
@override
PaperlessServerStatsApi createServerStatsApi(Dio dio,
{required int apiVersion}) {
return serverStatsApi;
}
@override
PaperlessTasksApi createTasksApi(Dio dio, {required int apiVersion}) {
return tasksApi;
}
@override
PaperlessUserApi createUserApi(Dio dio, {required int apiVersion}) {
return userApi;
}
}

View File

@@ -0,0 +1,25 @@
import 'dart:io';
import 'package:hive/hive.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
Future<void> initHive(Directory directory, String defaultLocale) async {
Hive.init(directory.path);
registerHiveAdapters();
await Hive.openBox<LocalUserAccount>(HiveBoxes.localUserAccount);
await Hive.openBox<LocalUserAppState>(HiveBoxes.localUserAppState);
await Hive.openBox<bool>(HiveBoxes.hintStateBox);
await Hive.openBox<String>(HiveBoxes.hosts);
final globalSettingsBox =
await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
if (!globalSettingsBox.hasValue) {
await globalSettingsBox.setValue(
GlobalSettings(preferredLocaleSubtag: defaultLocale),
);
}
}

View File

@@ -1,16 +1,16 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
class LanguageHeaderInterceptor extends Interceptor { class LanguageHeaderInterceptor extends Interceptor {
String preferredLocaleSubtag; final String Function() preferredLocaleSubtagBuilder;
LanguageHeaderInterceptor(this.preferredLocaleSubtag); LanguageHeaderInterceptor(this.preferredLocaleSubtagBuilder);
@override @override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) { void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
late String languages; late String languages;
if (preferredLocaleSubtag == "en") { if (preferredLocaleSubtagBuilder() == "en") {
languages = "en"; languages = "en";
} else { } else {
languages = "$preferredLocaleSubtag,en;q=0.7,en-US;q=0.6"; languages = "${preferredLocaleSubtagBuilder()},en;q=0.7,en-US;q=0.6";
} }
options.headers.addAll({"Accept-Language": languages}); options.headers.addAll({"Accept-Language": languages});
handler.next(options); handler.next(options);

View File

@@ -1,93 +1,14 @@
import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_api/src/interceptor/dio_http_error_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/dio_offline_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
/// Manages the security context, authentication and base request URL for abstract interface class SessionManager implements ChangeNotifier {
/// an underlying [Dio] client which is injected into all services Dio get client;
/// requiring authenticated access to the Paperless REST API.
class SessionManager extends ValueNotifier<Dio> {
Dio get client => value;
SessionManager([List<Interceptor> interceptors = const []])
: super(_initDio(interceptors));
static Dio _initDio(List<Interceptor> interceptors) {
//en- and decoded by utf8 by default
final Dio dio = Dio(
BaseOptions(
contentType: Headers.jsonContentType,
followRedirects: true,
maxRedirects: 10,
),
);
dio.options
..receiveTimeout = const Duration(seconds: 30)
..sendTimeout = const Duration(seconds: 60)
..responseType = ResponseType.json;
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient =
() => HttpClient()..badCertificateCallback = (cert, host, port) => true;
dio.interceptors.addAll([
...interceptors,
DioUnauthorizedInterceptor(),
DioHttpErrorInterceptor(),
DioOfflineInterceptor(),
RetryOnConnectionChangeInterceptor(dio: dio)
]);
return dio;
}
void updateSettings({ void updateSettings({
String? baseUrl, String? baseUrl,
String? authToken, String? authToken,
ClientCertificate? clientCertificate, ClientCertificate? clientCertificate,
}) {
if (clientCertificate != null) {
final context = SecurityContext()
..usePrivateKeyBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
)
..useCertificateChainBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
)
..setTrustedCertificatesBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
);
final adapter = IOHttpClientAdapter()
..createHttpClient = () => HttpClient(context: context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
client.httpClientAdapter = adapter;
}
if (baseUrl != null) {
client.options.baseUrl = baseUrl;
}
if (authToken != null) {
client.options.headers.addAll({
HttpHeaders.authorizationHeader: 'Token $authToken',
}); });
} void resetSettings();
notifyListeners();
}
void resetSettings() {
client.httpClientAdapter = IOHttpClientAdapter();
client.options.baseUrl = '';
client.options.headers.remove(HttpHeaders.authorizationHeader);
notifyListeners();
}
} }

View File

@@ -0,0 +1,96 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/interceptor/dio_offline_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
/// Manages the security context, authentication and base request URL for
/// an underlying [Dio] client which is injected into all services
/// requiring authenticated access to the Paperless REST API.
class SessionManagerImpl extends ValueNotifier<Dio> implements SessionManager {
@override
Dio get client => value;
SessionManagerImpl([List<Interceptor> interceptors = const []])
: super(_initDio(interceptors));
static Dio _initDio(List<Interceptor> interceptors) {
//en- and decoded by utf8 by default
final Dio dio = Dio(
BaseOptions(
contentType: Headers.jsonContentType,
followRedirects: true,
maxRedirects: 10,
),
);
dio.options
..receiveTimeout = const Duration(seconds: 30)
..sendTimeout = const Duration(seconds: 60)
..responseType = ResponseType.json;
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient =
() => HttpClient()..badCertificateCallback = (cert, host, port) => true;
dio.interceptors.addAll([
...interceptors,
DioUnauthorizedInterceptor(),
DioHttpErrorInterceptor(),
DioOfflineInterceptor(),
RetryOnConnectionChangeInterceptor(dio: dio)
]);
return dio;
}
@override
void updateSettings({
String? baseUrl,
String? authToken,
ClientCertificate? clientCertificate,
}) {
if (clientCertificate != null) {
final context = SecurityContext()
..usePrivateKeyBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
)
..useCertificateChainBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
)
..setTrustedCertificatesBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
);
final adapter = IOHttpClientAdapter()
..createHttpClient = () => HttpClient(context: context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
client.httpClientAdapter = adapter;
}
if (baseUrl != null) {
client.options.baseUrl = baseUrl;
}
if (authToken != null) {
client.options.headers.addAll({
HttpHeaders.authorizationHeader: 'Token $authToken',
});
}
notifyListeners();
}
@override
void resetSettings() {
client.httpClientAdapter = IOHttpClientAdapter();
client.options.baseUrl = '';
client.options.headers.remove(HttpHeaders.authorizationHeader);
notifyListeners();
}
}

View File

@@ -5,6 +5,7 @@ import 'package:dio/dio.dart';
import 'package:paperless_mobile/core/global/os_error_codes.dart'; import 'package:paperless_mobile/core/global/os_error_codes.dart';
import 'package:paperless_mobile/core/interceptor/server_reachability_error_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/server_reachability_error_interceptor.dart';
import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/security/session_manager_impl.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.dart'; import 'package:paperless_mobile/features/login/model/reachability_status.dart';
import 'package:rxdart/subjects.dart'; import 'package:rxdart/subjects.dart';
@@ -79,7 +80,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
} }
try { try {
SessionManager manager = SessionManager manager =
SessionManager([ServerReachabilityErrorInterceptor()]) SessionManagerImpl([ServerReachabilityErrorInterceptor()])
..updateSettings(clientCertificate: clientCertificate) ..updateSettings(clientCertificate: clientCertificate)
..client.options.connectTimeout = const Duration(seconds: 5) ..client.options.connectTimeout = const Duration(seconds: 5)
..client.options.receiveTimeout = const Duration(seconds: 5); ..client.options.receiveTimeout = const Duration(seconds: 5);

View File

@@ -92,6 +92,8 @@ class _DocumentNotesWidgetState extends State<DocumentNotesWidget> {
label: Text(S.of(context)!.addNote), label: Text(S.of(context)!.addNote),
onPressed: () async { onPressed: () async {
_formKey.currentState?.save(); _formKey.currentState?.save();
FocusScope.of(context).unfocus();
if (_formKey.currentState?.validate() ?? false) { if (_formKey.currentState?.validate() ?? false) {
setState(() { setState(() {
_isNoteSubmitting = true; _isNoteSubmitting = true;

View File

@@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/transient_error.dart';
import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart'; import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart';
@@ -13,6 +14,7 @@ import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
import 'package:paperless_mobile/core/database/tables/user_credentials.dart'; import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
import 'package:paperless_mobile/core/security/session_manager_impl.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart'; import 'package:paperless_mobile/features/logging/data/logger.dart';
import 'package:paperless_mobile/features/logging/utils/redaction_utils.dart'; import 'package:paperless_mobile/features/logging/utils/redaction_utils.dart';
import 'package:paperless_mobile/core/model/info_message_exception.dart'; import 'package:paperless_mobile/core/model/info_message_exception.dart';
@@ -83,7 +85,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
AuthenticatingStage.persistingLocalUserData)); AuthenticatingStage.persistingLocalUserData));
}, },
); );
} catch (e) { } on PaperlessApiException catch (exception, stackTrace) {
emit( emit(
AuthenticationErrorState( AuthenticationErrorState(
serverUrl: serverUrl, serverUrl: serverUrl,
@@ -207,8 +209,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
methodName: 'switchAccount', methodName: 'switchAccount',
); );
final sessionManager = SessionManager([ final SessionManager sessionManager = SessionManagerImpl([
LanguageHeaderInterceptor(locale), LanguageHeaderInterceptor(() => locale),
]); ]);
await _addUser( await _addUser(
localUserId, localUserId,
@@ -462,14 +464,12 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final authApi = _apiFactory.createAuthenticationApi(sessionManager.client); final authApi = _apiFactory.createAuthenticationApi(sessionManager.client);
await onPerformLogin?.call();
logger.fd( logger.fd(
"Fetching bearer token from the server...", "Fetching bearer token from the server...",
className: runtimeType.toString(), className: runtimeType.toString(),
methodName: '_addUser', methodName: '_addUser',
); );
await onPerformLogin?.call();
final token = await authApi.login( final token = await authApi.login(
username: credentials.username!, username: credentials.username!,
password: credentials.password!, password: credentials.password!,
@@ -486,7 +486,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
clientCertificate: clientCert, clientCertificate: clientCert,
authToken: token, authToken: token,
); );
final userAccountBox = final userAccountBox =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount); Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final userStateBox = final userStateBox =
@@ -586,12 +585,14 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
clientCertificate: clientCert, clientCertificate: clientCert,
), ),
); );
logger.fd( logger.fd(
"User credentials successfully saved.", "User credentials successfully saved.",
className: runtimeType.toString(), className: runtimeType.toString(),
methodName: '_addUser', methodName: '_addUser',
); );
}); });
final hostsBox = Hive.box<String>(HiveBoxes.hosts); final hostsBox = Hive.box<String>(HiveBoxes.hosts);
if (!hostsBox.values.contains(serverUrl)) { if (!hostsBox.values.contains(serverUrl)) {
await hostsBox.add(serverUrl); await hostsBox.add(serverUrl);

View File

@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
@@ -17,7 +16,6 @@ import 'package:paperless_mobile/features/login/model/client_certificate_form_mo
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.dart'; import 'package:paperless_mobile/features/login/model/reachability_status.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/login_settings_page.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
import 'package:paperless_mobile/generated/assets.gen.dart'; import 'package:paperless_mobile/generated/assets.gen.dart';
@@ -44,6 +42,7 @@ class AddAccountPage extends StatefulWidget {
final bool showLocalAccounts; final bool showLocalAccounts;
final Widget? bottomLeftButton; final Widget? bottomLeftButton;
const AddAccountPage({ const AddAccountPage({
Key? key, Key? key,
required this.onSubmit, required this.onSubmit,

View File

@@ -5,6 +5,7 @@ import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/keys.dart';
class ServerAddressFormField extends StatefulWidget { class ServerAddressFormField extends StatefulWidget {
static const String fkServerAddress = "serverAddress"; static const String fkServerAddress = "serverAddress";
@@ -59,7 +60,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField>
maxWidth: MediaQuery.sizeOf(context).width - 40, maxWidth: MediaQuery.sizeOf(context).width - 40,
); );
}, },
key: const ValueKey('login-server-address'), key: TestKeys.login.serverAddressFormField,
optionsBuilder: (textEditingValue) { optionsBuilder: (textEditingValue) {
return Hive.box<String>(HiveBoxes.hosts) return Hive.box<String>(HiveBoxes.hosts)
.values .values

19
lib/keys.dart Normal file
View File

@@ -0,0 +1,19 @@
import 'package:flutter/widgets.dart';
class TestKeys {
TestKeys._();
static final login = _LoginTestKeys();
}
class _LoginTestKeys {
final serverAddressFormField = const Key('login-server-address');
final continueButton = const Key('login-continue-button');
final usernameFormField = const Key('login-username');
final passwordFormField = const Key('login-password');
final loginButton = const Key('login-login-button');
final clientCertificateFormField = const Key('login-client-certificate');
final clientCertificatePassphraseFormField =
const Key('login-client-certificate-passphrase');
final loggingInScreen = const Key('login-logging-in-screen');
}

View File

@@ -24,6 +24,8 @@ import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/my_bloc_observer.dart'; import 'package:paperless_mobile/core/bloc/my_bloc_observer.dart';
import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
import 'package:paperless_mobile/core/database/hive/hive_initialization.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
@@ -31,7 +33,7 @@ import 'package:paperless_mobile/core/exception/server_message_exception.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart';
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
import 'package:paperless_mobile/core/notifier/go_router_refresh_stream.dart'; import 'package:paperless_mobile/core/security/session_manager_impl.dart';
import 'package:paperless_mobile/features/logging/data/formatted_printer.dart'; import 'package:paperless_mobile/features/logging/data/formatted_printer.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart'; import 'package:paperless_mobile/features/logging/data/logger.dart';
import 'package:paperless_mobile/features/logging/data/mirrored_file_output.dart'; import 'package:paperless_mobile/features/logging/data/mirrored_file_output.dart';
@@ -105,49 +107,15 @@ Future<void> performMigrations() async {
} }
} }
Future<void> _initHive() async { Future<void> initializeDefaultParameters() async {
await Hive.initFlutter();
await performMigrations();
registerHiveAdapters();
await Hive.openBox<LocalUserAccount>(HiveBoxes.localUserAccount);
await Hive.openBox<LocalUserAppState>(HiveBoxes.localUserAppState);
await Hive.openBox<bool>(HiveBoxes.hintStateBox);
await Hive.openBox<String>(HiveBoxes.hosts);
final globalSettingsBox =
await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
if (!globalSettingsBox.hasValue) {
await globalSettingsBox.setValue(
GlobalSettings(preferredLocaleSubtag: defaultPreferredLocale.toString()),
);
}
}
void main() async {
runZonedGuarded(() async {
Bloc.observer = MyBlocObserver(); Bloc.observer = MyBlocObserver();
WidgetsFlutterBinding.ensureInitialized();
await FileService.instance.initialize(); await FileService.instance.initialize();
logger = l.Logger( logger = l.Logger(
output: MirroredFileOutput(), output: MirroredFileOutput(),
printer: FormattedPrinter(), printer: FormattedPrinter(),
level: l.Level.trace, level: l.Level.trace,
filter: l.ProductionFilter(), filter: l.ProductionFilter(),
); );
Paint.enableDithering = true;
// if (kDebugMode) {
// // URL: http://localhost:3131
// // Login: admin:test
// await LocalMockApiServer(
// // RandomDelayGenerator(
// // const Duration(milliseconds: 100),
// // const Duration(milliseconds: 800),
// // ),
// )
// .start();
// }
packageInfo = await PackageInfo.fromPlatform(); packageInfo = await PackageInfo.fromPlatform();
@@ -157,13 +125,18 @@ void main() async {
if (Platform.isIOS) { if (Platform.isIOS) {
iosInfo = await DeviceInfoPlugin().iosInfo; iosInfo = await DeviceInfoPlugin().iosInfo;
} }
await _initHive();
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
final globalSettingsBox =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings);
final globalSettings = globalSettingsBox.getValue()!;
await findSystemLocale(); await findSystemLocale();
}
void main() async {
runZonedGuarded(() async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
final hiveDirectory = await getApplicationDocumentsDirectory();
final defaultLocale = defaultPreferredLocale.languageCode;
await initializeDefaultParameters();
await initHive(hiveDirectory, defaultLocale);
await performMigrations();
final connectivityStatusService = ConnectivityStatusServiceImpl( final connectivityStatusService = ConnectivityStatusServiceImpl(
Connectivity(), Connectivity(),
@@ -179,10 +152,10 @@ void main() async {
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
final languageHeaderInterceptor = LanguageHeaderInterceptor( final languageHeaderInterceptor = LanguageHeaderInterceptor(
globalSettings.preferredLocaleSubtag, () => Hive.globalSettingsBox.getValue()!.preferredLocaleSubtag,
); );
// Manages security context, required for self signed client certificates // Manages security context, required for self signed client certificates
final sessionManager = SessionManager([ final SessionManager sessionManager = SessionManagerImpl([
PrettyDioLogger( PrettyDioLogger(
compact: true, compact: true,
responseBody: false, responseBody: false,
@@ -195,21 +168,9 @@ void main() async {
languageHeaderInterceptor, languageHeaderInterceptor,
]); ]);
// Initialize Blocs/Cubits
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
// Load application settings and stored authentication data
await connectivityCubit.initialize();
final localNotificationService = LocalNotificationService(); final localNotificationService = LocalNotificationService();
await localNotificationService.initialize(); await localNotificationService.initialize();
//Update language header in interceptor on language change.
globalSettingsBox.listenable().addListener(() {
languageHeaderInterceptor.preferredLocaleSubtag =
globalSettings.preferredLocaleSubtag;
});
final apiFactory = PaperlessApiFactoryImpl(sessionManager); final apiFactory = PaperlessApiFactoryImpl(sessionManager);
final authenticationCubit = AuthenticationCubit( final authenticationCubit = AuthenticationCubit(
localAuthService, localAuthService,
@@ -219,34 +180,20 @@ void main() async {
localNotificationService, localNotificationService,
); );
runApp( runApp(
MultiProvider( AppEntrypoint(
providers: [ sessionManager: sessionManager,
ChangeNotifierProvider.value(value: sessionManager),
Provider<LocalAuthenticationService>.value(value: localAuthService),
Provider<ConnectivityStatusService>.value(
value: connectivityStatusService),
Provider<LocalNotificationService>.value(
value: localNotificationService),
Provider.value(value: DocumentChangedNotifier()),
],
child: MultiProvider(
providers: [
Provider<ConnectivityCubit>.value(value: connectivityCubit),
Provider.value(value: authenticationCubit),
],
child: GoRouterShell(
apiFactory: apiFactory, apiFactory: apiFactory,
), authenticationCubit: authenticationCubit,
), connectivityStatusService: connectivityStatusService,
localNotificationService: localNotificationService,
localAuthService: localAuthService,
), ),
); );
}, (error, stackTrace) { }, (error, stackTrace) {
if (error is StateError && if (error is StateError &&
error.message.contains("Cannot emit new states")) { error.message.contains("Cannot emit new states")) {
{
return; return;
} }
}
// Catches all unexpected/uncaught errors and prints them to the console. // Catches all unexpected/uncaught errors and prints them to the console.
final message = switch (error) { final message = switch (error) {
PaperlessApiException e => e.details ?? error.toString(), PaperlessApiException e => e.details ?? error.toString(),
@@ -262,9 +209,52 @@ void main() async {
}); });
} }
class AppEntrypoint extends StatelessWidget {
final PaperlessApiFactory apiFactory;
final AuthenticationCubit authenticationCubit;
final ConnectivityStatusService connectivityStatusService;
final LocalNotificationService localNotificationService;
final LocalAuthenticationService localAuthService;
final SessionManager sessionManager;
const AppEntrypoint({
super.key,
required this.apiFactory,
required this.authenticationCubit,
required this.connectivityStatusService,
required this.localNotificationService,
required this.localAuthService,
required this.sessionManager,
});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider.value(value: DocumentChangedNotifier()),
Provider.value(value: authenticationCubit),
Provider.value(
value: ConnectivityCubit(connectivityStatusService)..initialize(),
),
ChangeNotifierProvider.value(value: sessionManager),
Provider.value(value: connectivityStatusService),
Provider.value(value: localNotificationService),
Provider.value(value: localAuthService),
],
child: GoRouterShell(
apiFactory: apiFactory,
),
);
}
}
class GoRouterShell extends StatefulWidget { class GoRouterShell extends StatefulWidget {
final PaperlessApiFactory apiFactory; final PaperlessApiFactory apiFactory;
const GoRouterShell({super.key, required this.apiFactory});
const GoRouterShell({
super.key,
required this.apiFactory,
});
@override @override
State<GoRouterShell> createState() => _GoRouterShellState(); State<GoRouterShell> createState() => _GoRouterShellState();

View File

@@ -12,6 +12,7 @@ import 'package:paperless_mobile/features/login/view/login_to_existing_account_p
import 'package:paperless_mobile/features/login/view/verify_identity_page.dart'; import 'package:paperless_mobile/features/login/view/verify_identity_page.dart';
import 'package:paperless_mobile/features/login/view/widgets/login_transition_page.dart'; import 'package:paperless_mobile/features/login/view/widgets/login_transition_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/keys.dart';
import 'package:paperless_mobile/routing/navigation_keys.dart'; import 'package:paperless_mobile/routing/navigation_keys.dart';
import 'package:paperless_mobile/routing/routes.dart'; import 'package:paperless_mobile/routing/routes.dart';
part 'login_route.g.dart'; part 'login_route.g.dart';
@@ -108,6 +109,7 @@ class AuthenticatingRoute extends GoRouteData {
}; };
return NoTransitionPage( return NoTransitionPage(
child: LoginTransitionPage( child: LoginTransitionPage(
key: TestKeys.login.loggingInScreen,
text: text, text: text,
), ),
); );

View File

@@ -82,14 +82,14 @@ SystemUiOverlayStyle buildOverlayStyle(
Brightness.light => SystemUiOverlayStyle.dark.copyWith( Brightness.light => SystemUiOverlayStyle.dark.copyWith(
systemNavigationBarColor: color, systemNavigationBarColor: color,
systemNavigationBarDividerColor: color, systemNavigationBarDividerColor: color,
// statusBarColor: theme.colorScheme.background, statusBarColor: theme.colorScheme.background,
// statusBarColor: theme.colorScheme.background, // statusBarColor: theme.colorScheme.background,
// systemNavigationBarDividerColor: theme.colorScheme.surface, // systemNavigationBarDividerColor: theme.colorScheme.surface,
), ),
Brightness.dark => SystemUiOverlayStyle.light.copyWith( Brightness.dark => SystemUiOverlayStyle.light.copyWith(
systemNavigationBarColor: color, systemNavigationBarColor: color,
systemNavigationBarDividerColor: color, systemNavigationBarDividerColor: color,
// statusBarColor: theme.colorScheme.background, statusBarColor: theme.colorScheme.background,
// statusBarColor: theme.colorScheme.background, // statusBarColor: theme.colorScheme.background,
// systemNavigationBarDividerColor: theme.colorScheme.surface, // systemNavigationBarDividerColor: theme.colorScheme.surface,
), ),

View File

@@ -73,7 +73,7 @@ class DocumentModel extends Equatable {
this.userCanChange, this.userCanChange,
this.permissions, this.permissions,
this.customFields = const [], this.customFields = const [],
this.notes = const [] = const [], this.notes = const [],
}); });
factory DocumentModel.fromJson(Map<String, dynamic> json) => factory DocumentModel.fromJson(Map<String, dynamic> json) =>

View File

@@ -101,10 +101,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.2" version: "1.18.0"
colorfilter_generator: colorfilter_generator:
dependency: transitive dependency: transitive
description: description:
@@ -244,10 +244,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.10.0"
paperless_document_scanner: paperless_document_scanner:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -376,18 +376,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.0" version: "1.11.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
stream_transform: stream_transform:
dependency: transitive dependency: transitive
description: description:
@@ -416,10 +416,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.6.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -440,10 +440,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: web name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.4-beta" version: "0.3.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:
@@ -469,5 +469,5 @@ packages:
source: hosted source: hosted
version: "6.3.0" version: "6.3.0"
sdks: sdks:
dart: ">=3.1.0 <4.0.0" dart: ">=3.2.0-194.0.dev <4.0.0"
flutter: ">=3.13.0" flutter: ">=3.13.0"