diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f2a5e5..66fd15a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## NEXT - 1.1.0(36) + +- Update AGP and Gradle version for latest Android Studio Ladybug (2024.2.1 Patch 1) +- #35 | Android - opening only specific file types +- #39 | When sharing document, set file name +- #36 | Implement first version of Document validation + ## 2024-07-16 - v1.0.4(35) - #32 | Cleanup diff --git a/android/app/build.gradle b/android/app/build.gradle index be42fcb..026b8f1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -32,12 +32,12 @@ android { ndkVersion flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -49,7 +49,7 @@ android { // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion 28 - targetSdkVersion flutter.targetSdkVersion + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName ndk { @@ -59,7 +59,10 @@ android { signingConfigs { release { - storeFile file(System.getenv("AVM_KEYSTORE_FILE")) + def keystorePath = System.getenv("AVM_KEYSTORE_FILE") + if (keystorePath != null) { + storeFile file(keystorePath) + } storePassword System.getenv("AVM_KEYSTORE_PASSWORD") keyAlias System.getenv("AVM_KEY_ALIAS") keyPassword System.getenv("AVM_KEY_PASSWORD") @@ -69,21 +72,21 @@ android { buildTypes { release { signingConfig signingConfigs.release + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + + debug { + // no signing config for debug builds } } + ndkVersion "25.1.8937393" } repositories { google() mavenCentral() - maven { - // Local distribution - url = uri("../../../eidmsdk_flutter/android/libs") - } } flutter { source '../..' } - -dependencies {} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..07b3062 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,38 @@ +-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication +-dontwarn com.google.android.play.core.splitinstall.SplitInstallException +-dontwarn com.google.android.play.core.splitinstall.SplitInstallManager +-dontwarn com.google.android.play.core.splitinstall.SplitInstallManagerFactory +-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest$Builder +-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest +-dontwarn com.google.android.play.core.splitinstall.SplitInstallSessionState +-dontwarn com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener +-dontwarn com.google.android.play.core.tasks.OnFailureListener +-dontwarn com.google.android.play.core.tasks.OnSuccessListener +-dontwarn com.google.android.play.core.tasks.Task +-dontwarn com.google.api.client.http.GenericUrl +-dontwarn com.google.api.client.http.HttpHeaders +-dontwarn com.google.api.client.http.HttpRequest +-dontwarn com.google.api.client.http.HttpRequestFactory +-dontwarn com.google.api.client.http.HttpResponse +-dontwarn com.google.api.client.http.HttpTransport +-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder +-dontwarn com.google.api.client.http.javanet.NetHttpTransport +-dontwarn javax.naming.Binding +-dontwarn javax.naming.NamingEnumeration +-dontwarn javax.naming.NamingException +-dontwarn javax.naming.directory.Attribute +-dontwarn javax.naming.directory.Attributes +-dontwarn javax.naming.directory.DirContext +-dontwarn javax.naming.directory.InitialDirContext +-dontwarn javax.naming.directory.SearchControls +-dontwarn javax.naming.directory.SearchResult +-dontwarn org.bouncycastle.jsse.BCSSLParameters +-dontwarn org.bouncycastle.jsse.BCSSLSocket +-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider +-dontwarn org.conscrypt.Conscrypt$Version +-dontwarn org.conscrypt.Conscrypt +-dontwarn org.conscrypt.ConscryptHostnameVerifier +-dontwarn org.joda.time.Instant +-dontwarn org.openjsse.javax.net.ssl.SSLParameters +-dontwarn org.openjsse.javax.net.ssl.SSLSocket +-dontwarn org.openjsse.net.ssl.OpenJSSE diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index ec915a8..5e6b542 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index b9082bf..d31a1c9 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -23,7 +23,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.4.2" apply false + id "com.android.application" version "8.3.2" apply false // START: FlutterFire Configuration id "com.google.gms.google-services" version "4.3.15" apply false id "com.google.firebase.crashlytics" version "2.8.1" apply false diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/lib/bloc/document_validation_cubit.dart b/lib/bloc/document_validation_cubit.dart new file mode 100644 index 0000000..bec65fb --- /dev/null +++ b/lib/bloc/document_validation_cubit.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:autogram_sign/autogram_sign.dart'; +import 'package:flutter_bloc/flutter_bloc.dart' show Cubit; +import 'package:injectable/injectable.dart'; +import 'package:logging/logging.dart'; + +import '../ui/fragment/document_validation_fragment.dart'; +import 'document_validation_state.dart'; +import 'preview_document_cubit.dart'; + +export 'document_validation_state.dart'; + +/// Cubit for the [DocumentValidationFragment] with [validateDocument] function. +/// +/// See also: +/// - [PreviewDocumentCubit] +@injectable +class DocumentValidationCubit extends Cubit { + static final _log = Logger((DocumentValidationCubit).toString()); + + final IAutogramService _service; + + DocumentValidationCubit({ + required IAutogramService service, + }) : _service = service, + super(const DocumentValidationInitialState()); + + Future validateDocument(String documentId) async { + _log.info("Requesting to validate Document Id: '$documentId'."); + emit(const DocumentValidationLoadingState()); + + try { + final response = await _service.getDocumentValidation(documentId); + + _log.info( + "Got Document validation: (signatureForm: ${response.signatureForm?.value}, signatures: ${response.signatures?.map((e) => e.validationResult.value)})."); + + emit(DocumentValidationSuccessState(response)); + } catch (error, stackTrace) { + if (error is ServiceException && + error.errorCode == "DOCUMENT_NOT_SIGNED") { + _log.info("Cannot validate unsigned Document."); + + emit(const DocumentValidationNotSignedState()); + } else { + _log.severe("Error validating Document.", error, stackTrace); + + emit(DocumentValidationErrorState(error)); + } + } + } +} diff --git a/lib/bloc/document_validation_state.dart b/lib/bloc/document_validation_state.dart new file mode 100644 index 0000000..c0afe79 --- /dev/null +++ b/lib/bloc/document_validation_state.dart @@ -0,0 +1,50 @@ +import 'package:autogram_sign/autogram_sign.dart' + show DocumentValidationResponseBody; +import 'package:flutter/foundation.dart'; + +import 'document_validation_cubit.dart'; + +/// State for [DocumentValidationCubit]. +@immutable +sealed class DocumentValidationState { + const DocumentValidationState(); + + @override + String toString() { + return "$runtimeType()"; + } +} + +class DocumentValidationInitialState extends DocumentValidationState { + const DocumentValidationInitialState(); +} + +class DocumentValidationLoadingState extends DocumentValidationState { + const DocumentValidationLoadingState(); +} + +class DocumentValidationErrorState extends DocumentValidationState { + final Object error; + + const DocumentValidationErrorState(this.error); + + @override + String toString() { + return "$runtimeType(error: $error)"; + } +} + +class DocumentValidationNotSignedState extends DocumentValidationState { + const DocumentValidationNotSignedState(); +} + +class DocumentValidationSuccessState extends DocumentValidationState { + final DocumentValidationResponseBody response; + + const DocumentValidationSuccessState(this.response); + + @override + String toString() { + return "$runtimeType(response: $response)"; + } +} diff --git a/lib/bloc/present_signed_document_cubit.dart b/lib/bloc/present_signed_document_cubit.dart index 9de8cfb..a394a9a 100644 --- a/lib/bloc/present_signed_document_cubit.dart +++ b/lib/bloc/present_signed_document_cubit.dart @@ -11,6 +11,7 @@ import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import '../app_service.dart'; +import '../data/document_signing_type.dart'; import '../file_extensions.dart'; import '../file_system_entity_extensions.dart'; import '../ui/screens/present_signed_document_screen.dart'; @@ -34,8 +35,14 @@ class PresentSignedDocumentCubit extends Cubit { PresentSignedDocumentCubit({ required AppService appService, @factoryParam required this.signedDocument, + @factoryParam required DocumentSigningType signingType, }) : _appService = appService, - super(const PresentSignedDocumentInitialState()); + super( + signingType == DocumentSigningType.local + ? const PresentSignedDocumentInitialState() + // Remote documents are not saved locally, so we go directly to success state + : const PresentSignedRemoteDocumentSuccessState(), + ); /// Saves [signedDocument] into public directory. Future saveDocument() async { @@ -67,7 +74,7 @@ class PresentSignedDocumentCubit extends Cubit { Future getShareableFile() async { final state = this.state; - if (state is PresentSignedDocumentSuccessState) { + if (state is PresentSignedLocalDocumentSuccessState) { final file = state.file; if (await file.exists()) { @@ -103,6 +110,7 @@ class PresentSignedDocumentCubit extends Cubit { } /// Saves [signedDocument] content into given [file]. + // TODO As extension function on SignDocumentResponseBody type Future _saveDocumentIntoFile(File file) { return Future.microtask(() => base64Decode(signedDocument.content)) .then((bytes) => file.writeAsBytes(bytes, flush: true)); diff --git a/lib/bloc/present_signed_document_state.dart b/lib/bloc/present_signed_document_state.dart index cbe5684..3a7b12c 100644 --- a/lib/bloc/present_signed_document_state.dart +++ b/lib/bloc/present_signed_document_state.dart @@ -2,6 +2,8 @@ import 'dart:io' show File; import 'package:flutter/foundation.dart'; +import 'present_signed_document_cubit.dart'; + /// State for [PresentSignedDocumentCubit]. @immutable sealed class PresentSignedDocumentState { @@ -15,8 +17,8 @@ sealed class PresentSignedDocumentState { return PresentSignedDocumentErrorState(error); } - PresentSignedDocumentSuccessState toSuccess(File file) { - return PresentSignedDocumentSuccessState(file); + PresentSignedLocalDocumentSuccessState toSuccess(File file) { + return PresentSignedLocalDocumentSuccessState(file); } @override @@ -44,13 +46,19 @@ class PresentSignedDocumentErrorState extends PresentSignedDocumentState { } } -class PresentSignedDocumentSuccessState extends PresentSignedDocumentState { +class PresentSignedLocalDocumentSuccessState + extends PresentSignedDocumentState { final File file; - const PresentSignedDocumentSuccessState(this.file); + const PresentSignedLocalDocumentSuccessState(this.file); @override String toString() { return "$runtimeType(file: $file)"; } } + +class PresentSignedRemoteDocumentSuccessState + extends PresentSignedDocumentState { + const PresentSignedRemoteDocumentSuccessState(); +} diff --git a/lib/bloc/preview_document_cubit.dart b/lib/bloc/preview_document_cubit.dart index fe01598..51dacf1 100644 --- a/lib/bloc/preview_document_cubit.dart +++ b/lib/bloc/preview_document_cubit.dart @@ -31,7 +31,7 @@ class PreviewDocumentCubit extends Cubit { emit(state.toLoading()); try { - _log.info("Getting Document Visualisation for DocumentId: $documentId"); + _log.info("Getting Document Visualisation for Document Id: $documentId"); final visualization = await _service.getDocumentVisualization(documentId); diff --git a/lib/di.config.dart b/lib/di.config.dart index 2fc7be1..8c76c1e 100644 --- a/lib/di.config.dart +++ b/lib/di.config.dart @@ -8,28 +8,30 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i16; -import 'dart:io' as _i17; +import 'dart:async' as _i17; +import 'dart:io' as _i18; import 'package:autogram_sign/autogram_sign.dart' as _i7; import 'package:eidmsdk/eidmsdk.dart' as _i4; -import 'package:eidmsdk/types.dart' as _i13; -import 'package:flutter/foundation.dart' as _i12; +import 'package:eidmsdk/types.dart' as _i14; +import 'package:flutter/foundation.dart' as _i13; import 'package:get_it/get_it.dart' as _i1; import 'package:injectable/injectable.dart' as _i2; import 'app_service.dart' as _i3; -import 'bloc/create_document_cubit.dart' as _i15; -import 'bloc/get_document_signature_type_cubit.dart' as _i20; +import 'bloc/create_document_cubit.dart' as _i16; +import 'bloc/document_validation_cubit.dart' as _i20; +import 'bloc/get_document_signature_type_cubit.dart' as _i22; import 'bloc/paired_device_list_cubit.dart' as _i8; import 'bloc/present_signed_document_cubit.dart' as _i9; -import 'bloc/preview_document_cubit.dart' as _i10; -import 'bloc/select_signing_certificate_cubit.dart' as _i11; -import 'bloc/sign_document_cubit.dart' as _i14; -import 'data/pdf_signing_option.dart' as _i18; -import 'di.dart' as _i21; +import 'bloc/preview_document_cubit.dart' as _i11; +import 'bloc/select_signing_certificate_cubit.dart' as _i12; +import 'bloc/sign_document_cubit.dart' as _i15; +import 'data/document_signing_type.dart' as _i10; +import 'data/pdf_signing_option.dart' as _i19; +import 'di.dart' as _i23; import 'services/encryption_key_registry.dart' as _i5; -import 'use_case/get_document_signature_type_use_case.dart' as _i19; +import 'use_case/get_document_signature_type_use_case.dart' as _i21; import 'use_case/get_document_version_use_case.dart' as _i6; extension GetItInjectableX on _i1.GetIt { @@ -54,62 +56,65 @@ extension GetItInjectableX on _i1.GetIt { gh.factory<_i8.PairedDeviceListCubit>( () => _i8.PairedDeviceListCubit(service: gh<_i7.IAutogramService>())); gh.factoryParam<_i9.PresentSignedDocumentCubit, - _i7.SignDocumentResponseBody, dynamic>(( + _i7.SignDocumentResponseBody, _i10.DocumentSigningType>(( signedDocument, - _, + signingType, ) => _i9.PresentSignedDocumentCubit( appService: gh<_i3.AppService>(), signedDocument: signedDocument, + signingType: signingType, )); - gh.factoryParam<_i10.PreviewDocumentCubit, String, dynamic>(( + gh.factoryParam<_i11.PreviewDocumentCubit, String, dynamic>(( documentId, _, ) => - _i10.PreviewDocumentCubit( + _i11.PreviewDocumentCubit( service: gh<_i7.IAutogramService>(), documentId: documentId, )); - gh.factoryParam<_i11.SelectSigningCertificateCubit, - _i12.ValueNotifier<_i13.Certificate?>, dynamic>(( + gh.factoryParam<_i12.SelectSigningCertificateCubit, + _i13.ValueNotifier<_i14.Certificate?>, dynamic>(( signingCertificate, _, ) => - _i11.SelectSigningCertificateCubit( + _i12.SelectSigningCertificateCubit( eidmsdk: gh<_i4.Eidmsdk>(), signingCertificate: signingCertificate, )); - gh.factoryParam<_i14.SignDocumentCubit, String, _i13.Certificate>(( + gh.factoryParam<_i15.SignDocumentCubit, String, _i14.Certificate>(( documentId, certificate, ) => - _i14.SignDocumentCubit( + _i15.SignDocumentCubit( service: gh<_i7.IAutogramService>(), eidmsdk: gh<_i4.Eidmsdk>(), documentId: documentId, certificate: certificate, )); - gh.factoryParam<_i15.CreateDocumentCubit, _i16.FutureOr<_i17.File>, - _i18.PdfSigningOption>(( + gh.factoryParam<_i16.CreateDocumentCubit, _i17.FutureOr<_i18.File>, + _i19.PdfSigningOption>(( file, pdfSigningOption, ) => - _i15.CreateDocumentCubit( + _i16.CreateDocumentCubit( service: gh<_i7.IAutogramService>(), file: file, pdfSigningOption: pdfSigningOption, )); - gh.lazySingleton<_i19.GetDocumentSignatureTypeUseCase>( - () => _i19.GetDocumentSignatureTypeUseCase(gh<_i7.IAutogramService>())); - gh.factory<_i20.GetDocumentSignatureTypeCubit>(() => - _i20.GetDocumentSignatureTypeCubit( + gh.factory<_i20.DocumentValidationCubit>(() => + _i20.DocumentValidationCubit(service: gh<_i7.IAutogramService>())); + gh.lazySingleton<_i21.GetDocumentSignatureTypeUseCase>( + () => _i21.GetDocumentSignatureTypeUseCase(gh<_i7.IAutogramService>())); + gh.factory<_i22.GetDocumentSignatureTypeCubit>(() => + _i22.GetDocumentSignatureTypeCubit( getDocumentSignatureType: - gh<_i19.GetDocumentSignatureTypeUseCase>())); + gh<_i21.GetDocumentSignatureTypeUseCase>())); return this; } } -class _$ExtrernalModule extends _i21.ExtrernalModule { +class _$ExtrernalModule extends _i23.ExtrernalModule { @override _i4.Eidmsdk get eidmsdk => _i4.Eidmsdk(); } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 58b4080..8177680 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -217,6 +217,48 @@ abstract class AppLocalizations { /// **'Podpísať iným certifikátom'** String get buttonSignWithDifferentCertificateLabel; + /// No description provided for @buttonMenuLabelSemantics. + /// + /// In sk, this message translates to: + /// **'Otvoriť menu'** + String get buttonMenuLabelSemantics; + + /// No description provided for @qrCodeScannerTorchOnSemantics. + /// + /// In sk, this message translates to: + /// **'Zapnúť blesk'** + String get qrCodeScannerTorchOnSemantics; + + /// No description provided for @qrCodeScannerTorchOffSemantics. + /// + /// In sk, this message translates to: + /// **'Vypnúť blesk'** + String get qrCodeScannerTorchOffSemantics; + + /// No description provided for @qrCodeScannerBackSemantics. + /// + /// In sk, this message translates to: + /// **'Späť'** + String get qrCodeScannerBackSemantics; + + /// No description provided for @qrCodeScannerOpenSemantics. + /// + /// In sk, this message translates to: + /// **'Podpísať vzdialený dokument pomocou QR kódu'** + String get qrCodeScannerOpenSemantics; + + /// No description provided for @shareDocumentPreviewSemantics. + /// + /// In sk, this message translates to: + /// **'Zdieľať náhľad dokumentu'** + String get shareDocumentPreviewSemantics; + + /// No description provided for @previewDocumentSemantics. + /// + /// In sk, this message translates to: + /// **'Náhľad dokumentu'** + String get previewDocumentSemantics; + /// No description provided for @deepLinkParseErrorMessage. /// /// In sk, this message translates to: @@ -376,7 +418,7 @@ abstract class AppLocalizations { /// No description provided for @introBody. /// /// In sk, this message translates to: - /// **'Začnite výberom dokumentu na:\n ✅ Jednoduché podpisovanie'** + /// **'Začnite výberom dokumentu na:\n ✅ Jednoduché podpisovanie\n ✅ Rýchle overenie podpisov'** String get introBody; /// No description provided for @onboardingFinishedHeading. @@ -451,6 +493,42 @@ abstract class AppLocalizations { /// **'Neviem vizualizovať {type} typ.'** String documentVisualizationCannotVisualizeTypeError(Object type); + /// No description provided for @documentValidationLoadingLabel. + /// + /// In sk, this message translates to: + /// **'Prebieha overovanie podpisov'** + String get documentValidationLoadingLabel; + + /// No description provided for @documentValidationNoSignaturesLabel. + /// + /// In sk, this message translates to: + /// **'Dokument zatiaľ neobsahuje **žiadny podpis**'** + String get documentValidationNoSignaturesLabel; + + /// No description provided for @documentValidationHasInvalidSignaturesLabel. + /// + /// In sk, this message translates to: + /// **'Dokument obsahuje **neplatné podpisy**'** + String get documentValidationHasInvalidSignaturesLabel; + + /// No description provided for @documentValidationHasIndeterminateSignatureLabel. + /// + /// In sk, this message translates to: + /// **'Dokument obsahuje **neznámy podpis**'** + String get documentValidationHasIndeterminateSignatureLabel; + + /// No description provided for @documentValidationHasValidSignaturesLabel. + /// + /// In sk, this message translates to: + /// **'{count, plural, one {Dokument obsahuje **1 podpis**} few {Dokument obsahuje **{count} podpisy**} many {Dokument obsahuje {count} podpisov**} other {Dokument obsahuje **{count} podpisov**}}'** + String documentValidationHasValidSignaturesLabel(num count); + + /// No description provided for @documentValidationSignaturesHeading. + /// + /// In sk, this message translates to: + /// **'Podpisy v dokumente'** + String get documentValidationSignaturesHeading; + /// No description provided for @selectCertificateTitle. /// /// In sk, this message translates to: @@ -505,12 +583,90 @@ abstract class AppLocalizations { /// **'Vydavateľ: {text}'** String certificateIssuer(Object text); + /// No description provided for @certificateIssuerLabel. + /// + /// In sk, this message translates to: + /// **'Vydavateľ'** + String get certificateIssuerLabel; + + /// No description provided for @certificateSubjectLabel. + /// + /// In sk, this message translates to: + /// **'Vydaný pre'** + String get certificateSubjectLabel; + + /// No description provided for @certificateValidityLabel. + /// + /// In sk, this message translates to: + /// **'Platnosť'** + String get certificateValidityLabel; + + /// No description provided for @certificateValidityNotBeforeLabel. + /// + /// In sk, this message translates to: + /// **'Platný od'** + String get certificateValidityNotBeforeLabel; + + /// No description provided for @certificateValidityNotAfterLabel. + /// + /// In sk, this message translates to: + /// **'Platný do'** + String get certificateValidityNotAfterLabel; + /// No description provided for @certificateNotAfter. /// /// In sk, this message translates to: /// **'Platný do: {text}'** String certificateNotAfter(Object text); + /// No description provided for @validationResultPassedLabel. + /// + /// In sk, this message translates to: + /// **'Platný'** + String get validationResultPassedLabel; + + /// No description provided for @validationResultFailedLabel. + /// + /// In sk, this message translates to: + /// **'Neplatný'** + String get validationResultFailedLabel; + + /// No description provided for @validationResultIndeterminateLabel. + /// + /// In sk, this message translates to: + /// **'Neznámy'** + String get validationResultIndeterminateLabel; + + /// No description provided for @validationResultUnknownLabel. + /// + /// In sk, this message translates to: + /// **'Neznámy'** + String get validationResultUnknownLabel; + + /// No description provided for @signatureQualificationQesigLabel. + /// + /// In sk, this message translates to: + /// **'Vlastnoručný podpis'** + String get signatureQualificationQesigLabel; + + /// No description provided for @signatureQualificationQesigWithQTLabel. + /// + /// In sk, this message translates to: + /// **'Osvedčený podpis'** + String get signatureQualificationQesigWithQTLabel; + + /// No description provided for @signatureQualificationQesealWithQTLabel. + /// + /// In sk, this message translates to: + /// **'Elektronická pečať'** + String get signatureQualificationQesealWithQTLabel; + + /// No description provided for @signatureQualificationAdesigWithQTLabel. + /// + /// In sk, this message translates to: + /// **'Uznaný spôsob autorizácie'** + String get signatureQualificationAdesigWithQTLabel; + /// No description provided for @signDocumentTitle. /// /// In sk, this message translates to: @@ -553,12 +709,6 @@ abstract class AppLocalizations { /// **'Zdieľať podpísaný dokument'** String get shareSignedDocumentLabel; - /// No description provided for @shareSignedDocumentSubject. - /// - /// In sk, this message translates to: - /// **'Elektronicky podpísaný dokument'** - String get shareSignedDocumentSubject; - /// No description provided for @shareSignedDocumentText. /// /// In sk, this message translates to: diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index c869bf9..6088aed 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -71,6 +71,27 @@ class AppLocalizationsSk extends AppLocalizations { @override String get buttonSignWithDifferentCertificateLabel => 'Podpísať iným certifikátom'; + @override + String get buttonMenuLabelSemantics => 'Otvoriť menu'; + + @override + String get qrCodeScannerTorchOnSemantics => 'Zapnúť blesk'; + + @override + String get qrCodeScannerTorchOffSemantics => 'Vypnúť blesk'; + + @override + String get qrCodeScannerBackSemantics => 'Späť'; + + @override + String get qrCodeScannerOpenSemantics => 'Podpísať vzdialený dokument pomocou QR kódu'; + + @override + String get shareDocumentPreviewSemantics => 'Zdieľať náhľad dokumentu'; + + @override + String get previewDocumentSemantics => 'Náhľad dokumentu'; + @override String deepLinkParseErrorMessage(Object error) { return 'Nepodporovaný alebo nesprávny odkaz:\n$error'; @@ -194,7 +215,7 @@ class AppLocalizationsSk extends AppLocalizations { String get introHeading => 'Nový, lepší a krajší podpisovač v mobile'; @override - String get introBody => 'Začnite výberom dokumentu na:\n ✅ Jednoduché podpisovanie'; + String get introBody => 'Začnite výberom dokumentu na:\n ✅ Jednoduché podpisovanie\n ✅ Rýchle overenie podpisov'; @override String get onboardingFinishedHeading => 'Autogram je pripravený'; @@ -234,6 +255,34 @@ class AppLocalizationsSk extends AppLocalizations { return 'Neviem vizualizovať $type typ.'; } + @override + String get documentValidationLoadingLabel => 'Prebieha overovanie podpisov'; + + @override + String get documentValidationNoSignaturesLabel => 'Dokument zatiaľ neobsahuje **žiadny podpis**'; + + @override + String get documentValidationHasInvalidSignaturesLabel => 'Dokument obsahuje **neplatné podpisy**'; + + @override + String get documentValidationHasIndeterminateSignatureLabel => 'Dokument obsahuje **neznámy podpis**'; + + @override + String documentValidationHasValidSignaturesLabel(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Dokument obsahuje **$count podpisov**', + many: 'Dokument obsahuje $count podpisov**', + few: 'Dokument obsahuje **$count podpisy**', + one: 'Dokument obsahuje **1 podpis**', + ); + return '$_temp0'; + } + + @override + String get documentValidationSignaturesHeading => 'Podpisy v dokumente'; + @override String get selectCertificateTitle => 'Výber typu podpisu'; @@ -263,11 +312,50 @@ class AppLocalizationsSk extends AppLocalizations { return 'Vydavateľ: $text'; } + @override + String get certificateIssuerLabel => 'Vydavateľ'; + + @override + String get certificateSubjectLabel => 'Vydaný pre'; + + @override + String get certificateValidityLabel => 'Platnosť'; + + @override + String get certificateValidityNotBeforeLabel => 'Platný od'; + + @override + String get certificateValidityNotAfterLabel => 'Platný do'; + @override String certificateNotAfter(Object text) { return 'Platný do: $text'; } + @override + String get validationResultPassedLabel => 'Platný'; + + @override + String get validationResultFailedLabel => 'Neplatný'; + + @override + String get validationResultIndeterminateLabel => 'Neznámy'; + + @override + String get validationResultUnknownLabel => 'Neznámy'; + + @override + String get signatureQualificationQesigLabel => 'Vlastnoručný podpis'; + + @override + String get signatureQualificationQesigWithQTLabel => 'Osvedčený podpis'; + + @override + String get signatureQualificationQesealWithQTLabel => 'Elektronická pečať'; + + @override + String get signatureQualificationAdesigWithQTLabel => 'Uznaný spôsob autorizácie'; + @override String get signDocumentTitle => 'Podpisovanie dokumentu'; @@ -293,9 +381,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get shareSignedDocumentLabel => 'Zdieľať podpísaný dokument'; - @override - String get shareSignedDocumentSubject => 'Elektronicky podpísaný dokument'; - @override String get shareSignedDocumentText => '\n\nPodpísané aplikáciou Autogram v mobile'; diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 5ddffae..83822ef 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -22,6 +22,13 @@ "buttonSelectCertificateLabel": "Vybrať certifikát", "buttonSignWithCertificateLabel": "Podpísať ako {subject}", "buttonSignWithDifferentCertificateLabel": "Podpísať iným certifikátom", + "buttonMenuLabelSemantics": "Otvoriť menu", + "qrCodeScannerTorchOnSemantics": "Zapnúť blesk", + "qrCodeScannerTorchOffSemantics": "Vypnúť blesk", + "qrCodeScannerBackSemantics": "Späť", + "qrCodeScannerOpenSemantics": "Podpísať vzdialený dokument pomocou QR kódu", + "shareDocumentPreviewSemantics": "Zdieľať náhľad dokumentu", + "previewDocumentSemantics": "Náhľad dokumentu", "deepLinkParseErrorMessage": "Nepodporovaný alebo nesprávny odkaz:\n{error}", "stepIndicatorText": "Krok {stepNumber} z {totalSteps}", @@ -53,7 +60,7 @@ "thirdPartyLicensesLabel": "Licencie knižníc tretích strán", "introHeading": "Nový, lepší a krajší podpisovač v mobile", - "introBody": "Začnite výberom dokumentu na:\n ✅ Jednoduché podpisovanie", + "introBody": "Začnite výberom dokumentu na:\n ✅ Jednoduché podpisovanie\n ✅ Rýchle overenie podpisov", "onboardingFinishedHeading": "Autogram je pripravený", "onboardingFinishedBody": "Začnite výberom dokumentu na:\n ✅ Jednoduché podpisovanie", @@ -73,6 +80,13 @@ "documentVisualizationCannotVisualizeTypeError": "Neviem vizualizovať {type} typ.", + "documentValidationLoadingLabel": "Prebieha overovanie podpisov", + "documentValidationNoSignaturesLabel": "Dokument zatiaľ neobsahuje **žiadny podpis**", + "documentValidationHasInvalidSignaturesLabel": "Dokument obsahuje **neplatné podpisy**", + "documentValidationHasIndeterminateSignatureLabel": "Dokument obsahuje **neznámy podpis**", + "documentValidationHasValidSignaturesLabel": "{count, plural, one {Dokument obsahuje **1 podpis**} few {Dokument obsahuje **{count} podpisy**} many {Dokument obsahuje {count} podpisov**} other {Dokument obsahuje **{count} podpisov**}}", + "documentValidationSignaturesHeading": "Podpisy v dokumente", + "selectCertificateTitle": "Výber typu podpisu", "selectSigningCertificateTitle": "Nastavenie certifikátu", "selectSigningCertificateBody": "Na podpisovanie mobilom potrebujete disponovať vhodným podpisovým certifikátom. Podpisový certifikát si môžete nastaviť aj neskôr počas podpisovania prvého dokumentu.\n\n\nAk si prajete nastaviť podpisový certifikát teraz, pripravte si, prosím, občiansky preukaz a nasledujte inštrukcie na obrazovke.", @@ -84,8 +98,23 @@ "selectSigningCertificateErrorHeading": "Chyba pri načítavaní certifikátov z občianskeho preukazu.", "certificateIssuer": "Vydavateľ: {text}", + "certificateIssuerLabel": "Vydavateľ", + "certificateSubjectLabel": "Vydaný pre", + "certificateValidityLabel": "Platnosť", + "certificateValidityNotBeforeLabel": "Platný od", + "certificateValidityNotAfterLabel": "Platný do", "certificateNotAfter": "Platný do: {text}", + "validationResultPassedLabel": "Platný", + "validationResultFailedLabel": "Neplatný", + "validationResultIndeterminateLabel": "Neznámy", + "validationResultUnknownLabel": "Neznámy", + + "signatureQualificationQesigLabel": "Vlastnoručný podpis", + "signatureQualificationQesigWithQTLabel": "Osvedčený podpis", + "signatureQualificationQesealWithQTLabel": "Elektronická pečať", + "signatureQualificationAdesigWithQTLabel": "Uznaný spôsob autorizácie", + "signDocumentTitle": "Podpisovanie dokumentu", "signDocumentCanceledHeading": "Podpisovanie pomocou občianskeho preukazu bolo prerušené", "signDocumentErrorHeading": "Pri podpisovaní sa vyskytla chyba", @@ -95,7 +124,6 @@ "saveSignedDocumentErrorMessage": "Pri ukladaní súboru sa vyskytla chyba:\n{error}", "shareSignedDocumentLabel": "Zdieľať podpísaný dokument", - "shareSignedDocumentSubject": "Elektronicky podpísaný dokument", "shareSignedDocumentText": "\n\nPodpísané aplikáciou Autogram v mobile", "shareSignedDocumentErrorMessage": "Pri zdieľaní súboru sa vyskytla chyba:\n{error}", diff --git a/lib/oids.dart b/lib/oids.dart index 2d4641b..495d15f 100644 --- a/lib/oids.dart +++ b/lib/oids.dart @@ -8,6 +8,9 @@ class X500Oids { /// Organization name (ON). static const on = '2.5.4.10'; + /// Organization unit name (OU). + static const ou = '2.5.4.11'; + /// Serial number (SN). static const sn = '2.5.4.5'; diff --git a/lib/ui/fragment/document_validation_fragment.dart b/lib/ui/fragment/document_validation_fragment.dart new file mode 100644 index 0000000..f5ffd55 --- /dev/null +++ b/lib/ui/fragment/document_validation_fragment.dart @@ -0,0 +1,105 @@ +import 'package:autogram_sign/autogram_sign.dart' + show + DocumentValidationResponseBody, + DocumentValidationResponseBody$Signatures$Item, + DocumentValidationResponseBody$Signatures$ItemValidationResult; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../bloc/document_validation_cubit.dart'; +import '../../di.dart'; +import '../widgets/document_validation_strip.dart'; + +typedef _ValidationResult + = DocumentValidationResponseBody$Signatures$ItemValidationResult; + +/// Executes Document validation and displays output in [DocumentValidationStrip]. +/// +/// Uses [DocumentValidationCubit]. +class DocumentValidationFragment extends StatefulWidget { + final String documentId; + final ValueSetter + onShowDocumentValidationInfoRequested; + + const DocumentValidationFragment({ + super.key, + required this.documentId, + required this.onShowDocumentValidationInfoRequested, + }); + + @override + State createState() => + _DocumentValidationFragmentState(); +} + +class _DocumentValidationFragmentState + extends State { + bool _hidden = false; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + return getIt.get() + ..validateDocument(widget.documentId); + }, + child: BlocBuilder( + builder: _buildContent, + ), + ); + } + + Widget _buildContent(BuildContext context, DocumentValidationState state) { + if (_hidden) { + return const SizedBox.shrink(); + } + + return switch (state) { + DocumentValidationInitialState _ => const DocumentValidationStrip( + value: DocumentValidationStripValue.loading(), + ), + DocumentValidationLoadingState _ => const DocumentValidationStrip( + value: DocumentValidationStripValue.loading(), + ), + DocumentValidationNotSignedState _ => DocumentValidationStrip( + value: const DocumentValidationStripValue.none(), + onTap: () { + setState(() { + _hidden = true; + }); + }, + ), + DocumentValidationSuccessState state => DocumentValidationStrip( + value: DocumentValidationStripValue.value( + failedCount: state.response.failedCount, + indeterminateCount: state.response.indeterminateCount, + passedCount: state.response.passedCount, + ), + onTap: () { + widget.onShowDocumentValidationInfoRequested.call(state.response); + }, + ), + DocumentValidationErrorState _ => const SizedBox.shrink(), + // TODO Show error in this panel - red background with message + }; + } +} + +/// A set of extensions on [DocumentValidationResponseBody]. +extension _DocumentValidationResponseBodyExtensions + on DocumentValidationResponseBody { + List get signaturesOrEmpty => + (signatures ?? []); + + int get failedCount => signaturesOrEmpty + .where((s) => s.validationResult == _ValidationResult.totalFailed) + .length; + + int get indeterminateCount => signaturesOrEmpty + .where((s) => s.validationResult == _ValidationResult.indeterminate) + .length; + + int get passedCount => signaturesOrEmpty + .where((s) => s.validationResult == _ValidationResult.totalPassed) + .length; +} diff --git a/lib/ui/fragment/document_validation_info_fragment.dart b/lib/ui/fragment/document_validation_info_fragment.dart new file mode 100644 index 0000000..e2c91cc --- /dev/null +++ b/lib/ui/fragment/document_validation_info_fragment.dart @@ -0,0 +1,106 @@ +import 'package:autogram_sign/autogram_sign.dart'; +import 'package:flutter/material.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import '../../strings_context.dart'; +import '../widgets/document_signature_info.dart'; + +/// Displays Document validation info based on [data] provided +/// as list of [DocumentSignatureInfo] for each signature. +class DocumentValidationInfoFragment extends StatelessWidget { + final DocumentValidationResponseBody data; + + const DocumentValidationInfoFragment({ + super.key, + required this.data, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + context.strings.documentValidationSignaturesHeading, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Expanded( + child: _buildList(context), + ), + ], + ); + } + + Widget _buildList(BuildContext context) { + final signatures = (data.signatures ?? []); + + return ListView.separated( + itemCount: signatures.length, + itemBuilder: (context, index) { + final signature = signatures[index]; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: DocumentSignatureInfo(signature), + ); + }, + separatorBuilder: (context, _) { + return const Divider(); + }, + ); + } +} + +@widgetbook.UseCase( + path: '[Fragments]', + name: '', + type: DocumentValidationInfoFragment, +) +Widget previewDocumentValidationInfoFragment(BuildContext context) { + return const DocumentValidationInfoFragment( + data: DocumentValidationResponseBody( + containerType: DocumentValidationResponseBodyContainerType.asicE, + signatureForm: DocumentValidationResponseBodySignatureForm.xades, + signatures: [ + DocumentValidationResponseBody$Signatures$Item( + validationResult: + DocumentValidationResponseBody$Signatures$ItemValidationResult + .totalPassed, + level: DocumentValidationResponseBody$Signatures$ItemLevel + .xadesBaselineLta, + claimedSigningTime: "2023-08-01T12:37:47 +0200", + bestSigningTime: "2023-08-01T12:37:47 +0200", + signingCertificate: + DocumentValidationResponseBody$Signatures$Item$SigningCertificate( + qualification: + DocumentValidationResponseBody$Signatures$Item$SigningCertificateQualification + .qeseal, + issuerDN: + "OID.2.5.4.5=NTRCZ-26439395, O=\"První certifikační autorita, a.s.\", CN=I.CA Qualified CA/RSA 07/2015, C=CZ", + subjectDN: + "OID.2.5.4.5=ICA - 10432139, OID.2.5.4.97=NTRSK-00166073, CN=Ministerstvo spravodlivosti SR, O=Ministerstvo spravodlivosti SR, C=SK", + certificateDer: + "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", + ), + areQualifiedTimestamps: true, + timestamps: [ + DocumentValidationResponseBody$Signatures$Item$Timestamps$Item( + qualification: + DocumentValidationResponseBody$Signatures$Item$Timestamps$ItemQualification + .qtsa, + timestampType: + DocumentValidationResponseBody$Signatures$Item$Timestamps$ItemTimestampType + .signatureTimestamp, + subjectDN: + "CN=NASES Time Stamp Authority 2, O=Národná agentúra pre sieťové a elektronické služby, OID.2.5.4.97=NTRSK-42156424, OU=SNCA, C=SK", + certificateDer: + "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", + productionTime: "2024-08-02T13:01:24 +0200", + ), + ], + ), + ], + ), + ); +} diff --git a/lib/ui/fragment/preview_document_fragment.dart b/lib/ui/fragment/preview_document_fragment.dart new file mode 100644 index 0000000..a3a52ee --- /dev/null +++ b/lib/ui/fragment/preview_document_fragment.dart @@ -0,0 +1,102 @@ +import 'package:autogram_sign/autogram_sign.dart' + show DocumentVisualizationResponseBody; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import '../../bloc/preview_document_cubit.dart'; +import '../../strings_context.dart'; +import '../widgets/document_visualization.dart'; +import '../widgets/error_content.dart'; +import '../widgets/loading_content.dart'; + +/// Displays Document preview based on the [state]. +/// +/// See [PreviewDocumentCubit]. +class PreviewDocumentFragment extends StatelessWidget { + final PreviewDocumentState state; + + const PreviewDocumentFragment({ + super.key, + required this.state, + }); + + @override + Widget build(BuildContext context) { + return switch (state) { + PreviewDocumentInitialState _ => const LoadingContent(), + PreviewDocumentLoadingState _ => const LoadingContent(), + PreviewDocumentSuccessState state => _SuccessContent( + visualization: state.visualization, + ), + PreviewDocumentErrorState state => ErrorContent( + title: context.strings.previewDocumentErrorHeading, + error: state.error, + ), + }; + } +} + +/// Displays [visualization] using [DocumentVisualization] with dotted border. +class _SuccessContent extends StatelessWidget { + final DocumentVisualizationResponseBody visualization; + + const _SuccessContent({required this.visualization}); + + @override + Widget build(BuildContext context) { + final dashColor = Theme.of(context).colorScheme.primary; + + return Padding( + padding: const EdgeInsets.all(2), + child: DottedBorder( + color: dashColor, + strokeWidth: 4, + dashPattern: const [16, 16], + padding: EdgeInsets.zero, + child: DocumentVisualization( + visualization: visualization, + ), + ), + ); + } +} + +@widgetbook.UseCase( + path: '[Fragments]', + name: 'loading', + type: PreviewDocumentFragment, +) +Widget previewLoadingPreviewDocumentFragment(BuildContext context) { + return const PreviewDocumentFragment( + state: PreviewDocumentLoadingState(), + ); +} + +@widgetbook.UseCase( + path: '[Fragments]', + name: 'error', + type: PreviewDocumentFragment, +) +Widget previewErrorPreviewDocumentScreen(BuildContext context) { + return const PreviewDocumentFragment( + state: PreviewDocumentErrorState("Error message!"), + ); +} + +@widgetbook.UseCase( + path: '[Fragments]', + name: 'success', + type: PreviewDocumentFragment, +) +Widget previewSuccessPreviewDocumentScreen(BuildContext context) { + return const PreviewDocumentFragment( + state: PreviewDocumentSuccessState( + DocumentVisualizationResponseBody( + mimeType: "text/plain;base64", + filename: "sample.txt", + content: "", + ), + ), + ); +} diff --git a/lib/ui/screens/about_screen.dart b/lib/ui/screens/about_screen.dart index a39a645..f34cebc 100644 --- a/lib/ui/screens/about_screen.dart +++ b/lib/ui/screens/about_screen.dart @@ -40,10 +40,13 @@ class _Body extends StatelessWidget { final child = Column( children: [ - Text( - strings.appName, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineLarge, + Semantics( + header: true, + child: Text( + strings.appName, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineLarge, + ), ), const SizedBox(height: 16), const AppVersionText(), diff --git a/lib/ui/screens/certificate_details_dialog.dart b/lib/ui/screens/certificate_details_dialog.dart new file mode 100644 index 0000000..3b55a8f --- /dev/null +++ b/lib/ui/screens/certificate_details_dialog.dart @@ -0,0 +1,46 @@ +import 'package:basic_utils/basic_utils.dart' show TbsCertificate; +import 'package:flutter/material.dart'; + +import '../app_theme.dart'; +import '../widgets/certificate_details.dart'; + +/// Dialog that contains [CertificateDetails] widget. +class CertificateDetailsDialog extends StatelessWidget { + final TbsCertificate certificate; + + const CertificateDetailsDialog(this.certificate, {super.key}); + + @override + Widget build(BuildContext context) { + final body = SingleChildScrollView( + child: Padding( + padding: kScreenMargin, + child: CertificateDetails(certificate: certificate), + ), + ); + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: null, + actions: const [CloseButton()], + ), + body: SafeArea( + child: body, + ), + ); + } + + static Future show( + BuildContext context, + TbsCertificate certificate, + ) { + return showGeneralDialog( + context: context, + barrierDismissible: false, + pageBuilder: (context, __, ___) { + return CertificateDetailsDialog(certificate); + }, + ); + } +} diff --git a/lib/ui/screens/main_menu_screen.dart b/lib/ui/screens/main_menu_screen.dart index b49bb8b..c425627 100644 --- a/lib/ui/screens/main_menu_screen.dart +++ b/lib/ui/screens/main_menu_screen.dart @@ -29,12 +29,15 @@ class MainMenuScreen extends StatelessWidget { const Spacer(flex: 1), Padding( padding: const EdgeInsets.only(bottom: 20), - child: Text( - strings.menuTitle, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - letterSpacing: 0.15, + child: Semantics( + header: true, + child: Text( + strings.menuTitle, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + letterSpacing: 0.15, + ), ), ), ), @@ -157,11 +160,14 @@ class _MenuItem extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), - child: InkWell( - onTap: onPressed, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: text, + child: Semantics( + button: true, + child: InkWell( + onTap: onPressed, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: text, + ), ), ), ); diff --git a/lib/ui/screens/main_screen.dart b/lib/ui/screens/main_screen.dart index 1c85709..adddd8a 100644 --- a/lib/ui/screens/main_screen.dart +++ b/lib/ui/screens/main_screen.dart @@ -233,21 +233,31 @@ AppBar _MainAppBar({ return AppBar( foregroundColor: kMainAppBarForegroundColor, backgroundColor: kMainAppBarBackgroundColor, - leading: IconButton( - icon: SvgPicture.asset( - 'assets/icons/menu.svg', - colorFilter: colorFilter, + leading: Semantics( + button: true, + excludeSemantics: true, + label: context.strings.buttonMenuLabelSemantics, + child: IconButton( + icon: SvgPicture.asset( + 'assets/icons/menu.svg', + colorFilter: colorFilter, + ), + onPressed: onMenuPressed, ), - onPressed: onMenuPressed, ), actions: [ if (showQrCodeScannerIcon) - IconButton( - icon: SvgPicture.asset( - 'assets/icons/qr_code_scanner.svg', - colorFilter: colorFilter, + Semantics( + label: context.strings.qrCodeScannerOpenSemantics, + button: true, + excludeSemantics: true, + child: IconButton( + icon: SvgPicture.asset( + 'assets/icons/qr_code_scanner.svg', + colorFilter: colorFilter, + ), + onPressed: onQrCodeScannerPressed, ), - onPressed: onQrCodeScannerPressed, ), ], title: Builder(builder: (context) { @@ -292,7 +302,10 @@ class _Body extends StatelessWidget { style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 24), - Text(strings.introBody), + Text( + strings.introBody, + style: const TextStyle(height: 1.75), + ), const Spacer(), // Primary button diff --git a/lib/ui/screens/present_signed_document_screen.dart b/lib/ui/screens/present_signed_document_screen.dart index 23bad56..ceb6dc7 100644 --- a/lib/ui/screens/present_signed_document_screen.dart +++ b/lib/ui/screens/present_signed_document_screen.dart @@ -42,6 +42,7 @@ class PresentSignedDocumentScreen extends StatelessWidget { create: (context) { final cubit = getIt.get( param1: signedDocument, + param2: signingType, ); if (signingType == DocumentSigningType.local) { @@ -108,8 +109,10 @@ class PresentSignedDocumentScreen extends StatelessWidget { await Share.shareXFiles( [XFile(file.path)], - subject: strings.shareSignedDocumentSubject, text: strings.shareSignedDocumentText, + // It would be better to have something meaningful for email clients, + // however this is also used in Google Drive as target file name + subject: file.basename, ); } catch (error) { if (context.mounted) { @@ -161,11 +164,16 @@ class _Body extends StatelessWidget { onShareFileRequested: onShareFileRequested, onCloseRequested: onCloseRequested, ), - PresentSignedDocumentSuccessState state => _SuccessContent( + PresentSignedLocalDocumentSuccessState state => _SuccessContent( file: state.file, onShareFileRequested: onShareFileRequested, onCloseRequested: onCloseRequested, ), + PresentSignedRemoteDocumentSuccessState() => _SuccessContent( + file: null, + onShareFileRequested: null, + onCloseRequested: onCloseRequested, + ), }; } } @@ -345,7 +353,7 @@ Widget previewSuccessPresentSignedDocumentScreen(BuildContext context) { final file = File(path); return _Body( - state: PresentSignedDocumentSuccessState(file), + state: PresentSignedLocalDocumentSuccessState(file), signingType: signingType, onShareFileRequested: () { developer.log('onShareFileRequested'); diff --git a/lib/ui/screens/preview_document_screen.dart b/lib/ui/screens/preview_document_screen.dart index df411fc..4962857 100644 --- a/lib/ui/screens/preview_document_screen.dart +++ b/lib/ui/screens/preview_document_screen.dart @@ -1,30 +1,34 @@ import 'dart:io' show File; import 'package:autogram_sign/autogram_sign.dart' - show DocumentVisualizationResponseBody; -import 'package:dotted_border/dotted_border.dart'; + show DocumentValidationResponseBody; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; import '../../bloc/preview_document_cubit.dart'; import '../../data/document_signing_type.dart'; import '../../file_system_entity_extensions.dart'; import '../../strings_context.dart'; import '../app_theme.dart'; -import '../widgets/document_visualization.dart'; -import '../widgets/error_content.dart'; -import '../widgets/loading_content.dart'; +import '../fragment/document_validation_fragment.dart'; +import '../fragment/document_validation_info_fragment.dart'; +import '../fragment/preview_document_fragment.dart'; +import '../widgets/dialogs.dart' as avm; import 'open_document_screen.dart'; import 'select_certificate_screen.dart'; /// Screen for previewing single document from [file] and [documentId]. /// +/// Contains two fragments: +/// - [DocumentValidationFragment] +/// - [PreviewDocumentFragment] +/// /// Uses [PreviewDocumentCubit]. /// /// Navigates next to [SelectCertificateScreen]. +/// Shows [DocumentValidationInfoFragment]. /// /// See also: /// - [OpenDocumentScreen] @@ -40,7 +44,13 @@ class PreviewDocumentScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final body = BlocProvider( + final child1 = DocumentValidationFragment( + documentId: documentId, + onShowDocumentValidationInfoRequested: (data) { + _onShowDocumentValidationInfoRequested(context, data); + }, + ); + final child2 = BlocProvider( create: (context) { return GetIt.instance.get( param1: documentId, @@ -48,25 +58,35 @@ class PreviewDocumentScreen extends StatelessWidget { }, child: BlocBuilder( builder: (context, state) { - return _Body( + return _Content( state: state, - onSignRequested: () { - _onSignRequested(context); - }, + onSignRequested: () => _onSignRequested(context), ); }, ), ); + final body = Column( + children: [ + child1, + Expanded(child: child2), + ], + ); + return Scaffold( appBar: AppBar( title: Text(context.strings.previewDocumentTitle), actions: [ if (file != null) - IconButton( - onPressed: () => _onShareRequested(context), - icon: const Icon(Icons.share_outlined), - color: Theme.of(context).colorScheme.primary, + Semantics( + label: context.strings.shareDocumentPreviewSemantics, + excludeSemantics: true, + button: true, + child: IconButton( + onPressed: () => _onShareRequested(context), + icon: const Icon(Icons.share_outlined), + color: Theme.of(context).colorScheme.primary, + ), ), ], ), @@ -76,6 +96,19 @@ class PreviewDocumentScreen extends StatelessWidget { ); } + Future _onShowDocumentValidationInfoRequested( + BuildContext context, + DocumentValidationResponseBody data, + ) { + return avm.showModalBottomSheet( + context: context, + child: SizedBox( + height: 240, + child: DocumentValidationInfoFragment(data: data), + ), + ); + } + Future _onShareRequested(BuildContext context) async { final file = this.file; @@ -102,117 +135,38 @@ class PreviewDocumentScreen extends StatelessWidget { } } -/// [PreviewDocumentScreen] body. -class _Body extends StatelessWidget { +/// [PreviewDocumentScreen] content. +class _Content extends StatelessWidget { final PreviewDocumentState state; final VoidCallback? onSignRequested; - const _Body({required this.state, required this.onSignRequested}); - - @override - Widget build(BuildContext context) { - final child = switch (state) { - PreviewDocumentInitialState _ => const LoadingContent(), - PreviewDocumentLoadingState _ => const LoadingContent(), - PreviewDocumentSuccessState state => _SuccessContent( - visualization: state.visualization, - onSignRequested: onSignRequested, - ), - PreviewDocumentErrorState state => ErrorContent( - title: context.strings.previewDocumentErrorHeading, - error: state.error, - ), - }; - - return Padding( - padding: kScreenMargin, - child: child, - ); - } -} - -class _SuccessContent extends StatelessWidget { - final DocumentVisualizationResponseBody visualization; - final VoidCallback? onSignRequested; - - const _SuccessContent({ - required this.visualization, + const _Content({ + required this.state, required this.onSignRequested, }); @override Widget build(BuildContext context) { - final dashColor = Theme.of(context).colorScheme.primary; - return Column( children: [ // Document preview Expanded( - child: Padding( - padding: const EdgeInsets.only(bottom: 16), - child: DottedBorder( - color: dashColor, - strokeWidth: 4, - dashPattern: const [16, 16], - padding: const EdgeInsets.all(2), - child: DocumentVisualization( - visualization: visualization, - ), - ), - ), + child: PreviewDocumentFragment(state: state), ), // Primary button - FilledButton( - style: FilledButton.styleFrom( - minimumSize: kPrimaryButtonMinimumSize, + if (state is PreviewDocumentSuccessState) + Padding( + padding: kScreenMargin, + child: FilledButton( + style: FilledButton.styleFrom( + minimumSize: kPrimaryButtonMinimumSize, + ), + onPressed: () => onSignRequested?.call(), + child: Text(context.strings.buttonSignLabel), + ), ), - onPressed: onSignRequested, - child: Text(context.strings.buttonSignLabel), - ), ], ); } } - -@widgetbook.UseCase( - path: '[Screens]', - name: 'loading', - type: PreviewDocumentScreen, -) -Widget previewLoadingPreviewDocumentScreen(BuildContext context) { - return const _Body( - state: PreviewDocumentLoadingState(), - onSignRequested: null, - ); -} - -@widgetbook.UseCase( - path: '[Screens]', - name: 'error', - type: PreviewDocumentScreen, -) -Widget previewErrorPreviewDocumentScreen(BuildContext context) { - return const _Body( - state: PreviewDocumentErrorState("Error message!"), - onSignRequested: null, - ); -} - -@widgetbook.UseCase( - path: '[Screens]', - name: 'success', - type: PreviewDocumentScreen, -) -Widget previewSuccessPreviewDocumentScreen(BuildContext context) { - return const _Body( - state: PreviewDocumentSuccessState( - DocumentVisualizationResponseBody( - mimeType: "text/plain;base64", - filename: "sample.txt", - content: "", - ), - ), - onSignRequested: null, - ); -} diff --git a/lib/ui/screens/qr_code_scanner_screen.dart b/lib/ui/screens/qr_code_scanner_screen.dart index e860c17..ef2e39f 100644 --- a/lib/ui/screens/qr_code_scanner_screen.dart +++ b/lib/ui/screens/qr_code_scanner_screen.dart @@ -58,13 +58,18 @@ class _QRCodeScannerScreenState extends State { padding: kScreenMargin.copyWith( top: MediaQuery.of(context).padding.top, ), - child: SquareButton( - onPressed: () { - Navigator.maybePop(context); - }, - child: Icon( - Icons.arrow_back, - color: Theme.of(context).colorScheme.onBackground, + child: Semantics( + label: context.strings.qrCodeScannerBackSemantics, + button: true, + excludeSemantics: true, + child: SquareButton( + onPressed: () { + Navigator.maybePop(context); + }, + child: Icon( + Icons.arrow_back, + color: Theme.of(context).colorScheme.onBackground, + ), ), ), ), @@ -93,9 +98,20 @@ class _QRCodeScannerScreenState extends State { TorchState.on => Icons.flashlight_off, }; - return Icon( - icon, - color: Theme.of(context).colorScheme.onBackground, + final semanticsLabel = switch (torchState) { + TorchState.off => + context.strings.qrCodeScannerTorchOnSemantics, + TorchState.on => + context.strings.qrCodeScannerTorchOffSemantics, + }; + + return Semantics( + button: true, + label: semanticsLabel, + child: Icon( + icon, + color: Theme.of(context).colorScheme.onBackground, + ), ); }, ), diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart index a1922c8..0a6ebc4 100644 --- a/lib/ui/screens/settings_screen.dart +++ b/lib/ui/screens/settings_screen.dart @@ -105,11 +105,7 @@ class _Body extends StatelessWidget { final cert = value.tbsCertificate; - return [ - cert.subject[X500Oids.cn], - cert.subject[X500Oids.ln], - cert.subject[X500Oids.c], - ].whereType().join(", "); + return cert.subject[X500Oids.cn]; }(), onPressed: null, ); @@ -191,12 +187,15 @@ class _ValueListenableBoundTile extends StatelessWidget { builder: (context, value, _) { final summary = summaryGetter(setting.value); - return PreferenceTile( - title: title, - summary: summary, - onPressed: () { - _onEditItemRequested(context, value); - }, + return Semantics( + button: true, + child: PreferenceTile( + title: title, + summary: summary, + onPressed: () { + _onEditItemRequested(context, value); + }, + ), ); }, ); @@ -242,7 +241,7 @@ class _ValueListenableBoundTile extends StatelessWidget { ); return AlertDialog( - title: Text(title), + title: Semantics(header: true, child: Text(title)), content: content, ); }, diff --git a/lib/ui/widgets/certificate_details.dart b/lib/ui/widgets/certificate_details.dart new file mode 100644 index 0000000..554623b --- /dev/null +++ b/lib/ui/widgets/certificate_details.dart @@ -0,0 +1,95 @@ +import 'package:basic_utils/basic_utils.dart' show TbsCertificate; +import 'package:flutter/material.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import '../../oids.dart'; +import '../../strings_context.dart'; +import '../../utils.dart'; + +/// Displays X509 Certificate details: +/// - Issued to (Subject) +/// - Issued by +/// - Validity +class CertificateDetails extends StatelessWidget { + final TbsCertificate certificate; + + const CertificateDetails({super.key, required this.certificate}); + + @override + Widget build(BuildContext context) { + final strings = context.strings; + + final children = [ + _buildHeadline(strings.certificateSubjectLabel), + ..._buildSubjectInfo(certificate.subject), + _buildHeadline(strings.certificateIssuerLabel), + ..._buildSubjectInfo(certificate.issuer), + _buildHeadline(strings.certificateValidityLabel), + _Info(strings.certificateValidityNotBeforeLabel, + certificate.validity.notBefore.toString()), + _Info(strings.certificateValidityNotAfterLabel, + certificate.validity.notAfter.toString()), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ); + } + + Widget _buildHeadline(String text) { + return Padding( + padding: const EdgeInsets.only(top: 8, bottom: 2), + child: Text( + text, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + ), + ); + } + + List _buildSubjectInfo(Map data) { + return [ + _Info("Common name (CN)", data[X500Oids.cn]), + _Info("Organization (O)", data[X500Oids.on]), + _Info("Locality name (LN)", data[X500Oids.ln]), + _Info("Country (C)", data[X500Oids.c]), + _Info("Serial number (SN)", data[X500Oids.sn]), + ]; + } +} + +/// Wrapped row with [label] on the left and [value] on the right. +class _Info extends StatelessWidget { + final String label; + final String? value; + + const _Info(this.label, this.value); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 24), + child: Wrap( + spacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + SizedBox(width: 160, child: Text(label)), + Text(value ?? ''), + ], + ), + ); + } +} + +@widgetbook.UseCase( + path: '[Core]', + name: 'CertificateDetails', + type: CertificateDetails, +) +Widget previewCertificateDetails(BuildContext context) { + const certDer = + "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"; + final cert = x509CertificateDataFromDer(certDer).tbsCertificate!; + + return CertificateDetails(certificate: cert); +} diff --git a/lib/ui/widgets/certificate_picker_item.dart b/lib/ui/widgets/certificate_picker_item.dart index a6e5c98..8dab274 100644 --- a/lib/ui/widgets/certificate_picker_item.dart +++ b/lib/ui/widgets/certificate_picker_item.dart @@ -31,19 +31,16 @@ class CertificatePickerItem extends StatelessWidget { Widget build(BuildContext context) { final cert = certificate.tbsCertificate; - // NOT using RadioListTile because need to scale-up and style Radio - - final title = [ - cert.subject[X500Oids.cn], - cert.subject[X500Oids.ln], - cert.subject[X500Oids.c], - ].whereType().join(", "); final strings = context.strings; + + final title = cert.subject[X500Oids.cn] ?? ''; final identity = "${cert.subject[X500Oids.sn]}"; final issuer = strings.certificateIssuer("${cert.issuer[X500Oids.cn]}"); final validTo = strings.certificateNotAfter(_dateFormat.format(cert.validity.notAfter)); + // NOT using RadioListTile because need to scale-up and style Radio + return ListTile( onTap: () { onCertificateChanged(certificate); diff --git a/lib/ui/widgets/chip.dart b/lib/ui/widgets/chip.dart new file mode 100644 index 0000000..9181386 --- /dev/null +++ b/lib/ui/widgets/chip.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart' as material; +import 'package:flutter/widgets.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +/// Chip widget with [label] text inside. +/// Has [foreground], [background] and [border] colors, and optional +/// [leading] widget. +class Chip extends StatelessWidget { + final Widget? leading; + final String label; + final Color foreground; + final Color background; + final Color border; + + const Chip({ + super.key, + this.leading, + required this.label, + required this.foreground, + required this.background, + required this.border, + }); + + @override + material.Widget build(material.BuildContext context) { + return Container( + decoration: BoxDecoration( + color: background, + border: Border.all(color: border, width: 1), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + padding: const EdgeInsets.only(left: 8, top: 6, bottom: 6, right: 12), + child: Row( + children: [ + if (leading != null) leading!, + if (leading != null) const SizedBox(width: 8), + Text(label, style: TextStyle(color: foreground)), + ], + ), + ); + } +} + +@widgetbook.UseCase( + path: '[AVM]', + name: '', + type: Chip, +) +Widget previewChip(BuildContext context) { + var label = context.knobs.string( + label: 'Label', + initialValue: 'Chip!', + ); + var foreground = context.knobs.color( + label: 'Foreground', + initialValue: material.Colors.black, + ); + var background = context.knobs.color( + label: 'Background', + initialValue: material.Colors.white, + ); + var border = context.knobs.color( + label: 'Border', + initialValue: material.Colors.black, + ); + var icon = context.knobs.listOrNull( + label: 'Icon', + options: ['ok', 'warning', 'error'], + ); + + return Chip( + label: label, + foreground: foreground, + background: background, + border: border, + leading: (icon != null + ? Icon(switch (icon) { + 'ok' => material.Icons.check, + 'warning' => material.Icons.warning_amber_outlined, + 'error' => material.Icons.error_outline, + _ => material.Icons.question_mark, + }) + : null), + ); +} diff --git a/lib/ui/widgets/dialogs.dart b/lib/ui/widgets/dialogs.dart index 7cc025a..d0fb832 100644 --- a/lib/ui/widgets/dialogs.dart +++ b/lib/ui/widgets/dialogs.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart' as material; import 'package:flutter/material.dart'; import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; @@ -43,15 +44,15 @@ Future showNotificationsPermissionRationaleModal(BuildContext context) { ], ); - return _showModalBottomSheet( + return showModalBottomSheet( context: context, child: child, ); } -/// Calls Material [showModalBottomSheet] with predefined [child] wrapper +/// Calls Material `showModalBottomSheet` with predefined [child] wrapper /// and [avm.CloseButton]. -Future _showModalBottomSheet({ +Future showModalBottomSheet({ required BuildContext context, required Widget child, }) { @@ -67,7 +68,7 @@ Future _showModalBottomSheet({ ), ); - return showModalBottomSheet( + return material.showModalBottomSheet( context: context, isScrollControlled: true, constraints: const BoxConstraints(minWidth: double.infinity), diff --git a/lib/ui/widgets/document_signature_info.dart b/lib/ui/widgets/document_signature_info.dart new file mode 100644 index 0000000..cbd6f88 --- /dev/null +++ b/lib/ui/widgets/document_signature_info.dart @@ -0,0 +1,183 @@ +import 'package:autogram_sign/autogram_sign.dart'; +import 'package:basic_utils/basic_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import '../../oids.dart'; +import '../../strings_context.dart'; +import '../../utils.dart'; +import '../widgets/chip.dart' as avm; + +typedef SigningCertificateQualification + = DocumentValidationResponseBody$Signatures$Item$SigningCertificateQualification; +typedef _ValidationResult + = DocumentValidationResponseBody$Signatures$ItemValidationResult; + +/// Displays Document validation info based on provided [DocumentValidationResponseBody$Signatures$Item]. +/// +/// On left side, there is subject; validation result with qualification are +/// on right side. +class DocumentSignatureInfo extends StatelessWidget { + final TbsCertificate certificate; + final _ValidationResult validationResult; + final SigningCertificateQualification qualification; + final bool areQualifiedTimestamps; + + const DocumentSignatureInfo._({ + required this.certificate, + required this.validationResult, + required this.qualification, + required this.areQualifiedTimestamps, + }); + + factory DocumentSignatureInfo( + DocumentValidationResponseBody$Signatures$Item data, + ) { + final certDer = data.signingCertificate.certificateDer; + final certificate = x509CertificateDataFromDer(certDer).tbsCertificate!; + + return DocumentSignatureInfo._( + certificate: certificate, + validationResult: data.validationResult, + qualification: data.signingCertificate.qualification, + areQualifiedTimestamps: data.areQualifiedTimestamps, + ); + } + + @override + Widget build(BuildContext context) { + final subject = certificate.subject[X500Oids.cn] ?? ''; + final label = Text( + subject, + style: const TextStyle(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child: label), + const SizedBox(width: 8), + _buildChip(context), + ], + ); + } + + Widget _buildChip(BuildContext context) { + final icon = switch (validationResult) { + _ValidationResult.totalPassed => Icons.check, + _ValidationResult.indeterminate => Icons.warning_amber_outlined, + _ValidationResult.totalFailed => Icons.error_outline, + _ => Icons.question_mark_outlined, + }; + final foreground = switch (validationResult) { + _ValidationResult.totalPassed => const Color(0xFF033608), + _ValidationResult.indeterminate => const Color(0xFF4E2A00), + _ValidationResult.totalFailed => const Color(0xFF4E0711), + _ => Colors.black, + }; + final background = switch (validationResult) { + _ValidationResult.totalPassed => const Color(0xFFEDF5F3), + _ValidationResult.indeterminate => const Color(0xFFF4F4EC), + _ValidationResult.totalFailed => const Color(0xFFFBEEF0), + _ => Colors.white, + }; + final border = switch (validationResult) { + _ValidationResult.totalPassed => const Color(0xFFA9D9CD), + _ValidationResult.indeterminate => const Color(0xFFD5D6A2), + _ValidationResult.totalFailed => const Color(0xFFC3112B), + _ => Colors.grey, + }; + + return avm.Chip( + label: _getLabel(context), + foreground: foreground, + background: background, + border: border, + leading: Icon(icon, color: foreground), + ); + } + + String _getLabel(BuildContext context) { + final strings = context.strings; + + return switch (validationResult) { + _ValidationResult.totalFailed => strings.validationResultFailedLabel, + _ValidationResult.indeterminate => + strings.validationResultIndeterminateLabel, + _ValidationResult.swaggerGeneratedUnknown => + strings.validationResultUnknownLabel, + _ValidationResult.totalPassed => switch (qualification) { + SigningCertificateQualification.qesig => (!areQualifiedTimestamps + ? strings.signatureQualificationQesigLabel + : strings.signatureQualificationQesigWithQTLabel), + SigningCertificateQualification.qeseal => (areQualifiedTimestamps + ? strings.signatureQualificationQesealWithQTLabel + : ""), + SigningCertificateQualification.adesigQcQc => (areQualifiedTimestamps + ? strings.signatureQualificationAdesigWithQTLabel + : ""), + _ => "" + // TODO handle other cases properly + // https://github.com/slovensko-digital/autogram/blob/748d17c4c8d1b14516dba76bbb8f7beaadbc1bf6/src/main/java/digital/slovensko/autogram/ui/gui/SignatureBadgeFactory.java#L116 + } + }; + } +} + +@widgetbook.UseCase( + path: '[Core]', + name: '', + type: DocumentSignatureInfo, +) +Widget previewDocumentSignatureInfo(BuildContext context) { + final validationResult = _ValidationResult.values.byName(context.knobs.list( + label: "Validation result", + options: _ValidationResult.values.map((e) => e.name).toList(), + )); + final qualification = + SigningCertificateQualification.values.byName(context.knobs.list( + label: "Qualification", + options: SigningCertificateQualification.values.map((e) => e.name).toList(), + )); + final areQualifiedTimestamps = context.knobs.boolean(label: "TS qualified"); + + return DocumentSignatureInfo( + DocumentValidationResponseBody$Signatures$Item( + validationResult: validationResult, + level: + DocumentValidationResponseBody$Signatures$ItemLevel.xadesBaselineLta, + claimedSigningTime: "2023-08-01T12:37:47 +0200", + bestSigningTime: "2023-08-01T12:37:47 +0200", + signingCertificate: + DocumentValidationResponseBody$Signatures$Item$SigningCertificate( + qualification: qualification, + issuerDN: + "OID.2.5.4.5=NTRCZ-26439395, O=\"První certifikační autorita, a.s.\", CN=I.CA Qualified CA/RSA 07/2015, C=CZ", + subjectDN: + "OID.2.5.4.5=ICA - 10432139, OID.2.5.4.97=NTRSK-00166073, CN=Ministerstvo spravodlivosti SR, O=Ministerstvo spravodlivosti SR, C=SK", + certificateDer: + "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", + ), + areQualifiedTimestamps: areQualifiedTimestamps, + timestamps: const [ + DocumentValidationResponseBody$Signatures$Item$Timestamps$Item( + qualification: + DocumentValidationResponseBody$Signatures$Item$Timestamps$ItemQualification + .qtsa, + timestampType: + DocumentValidationResponseBody$Signatures$Item$Timestamps$ItemTimestampType + .signatureTimestamp, + subjectDN: + "CN=NASES Time Stamp Authority 2, O=Národná agentúra pre sieťové a elektronické služby, OID.2.5.4.97=NTRSK-42156424, OU=SNCA, C=SK", + certificateDer: + "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", + productionTime: "2024-08-02T13:01:24 +0200", + ), + ], + ), + ); +} diff --git a/lib/ui/widgets/document_validation_strip.dart b/lib/ui/widgets/document_validation_strip.dart new file mode 100644 index 0000000..75c8988 --- /dev/null +++ b/lib/ui/widgets/document_validation_strip.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import '../../strings_context.dart'; +import 'loading_indicator.dart'; +import 'markdown_text.dart'; + +/// Presents state of Document validation - displays text and potentially +/// loading indicator on the left and on the right one of the: +/// - arrow on the right when has any signatures +/// - × when has no signatures +/// +/// [onTap] is called when taped on whole widget. +class DocumentValidationStrip extends StatelessWidget { + final DocumentValidationStripValue value; + final VoidCallback? onTap; + + const DocumentValidationStrip({ + super.key, + required this.value, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final data = ( + value.isLoading, + value.failedCount, + value.indeterminateCount, + value.passedCount + ); + final isLoading = value.isLoading; + final hasSignatures = + (value.failedCount + value.indeterminateCount + value.passedCount) > 0; + final strings = context.strings; + final (String text, Color backgroundColor) = switch (data) { + (true, _, _, _) => ( + strings.documentValidationLoadingLabel, + const Color(0xFF126DFF) + ), + (false, 0, 0, 0) => ( + strings.documentValidationNoSignaturesLabel, + const Color(0xFF126DFF), + ), + (false, > 0, _, _) => ( + strings.documentValidationHasInvalidSignaturesLabel, + const Color(0xFFC3112B), + ), + (false, _, > 0, _) => ( + strings.documentValidationHasIndeterminateSignatureLabel, + const Color(0xFFbd730c), + ), + (false, 0, 0, > 0) => ( + strings.documentValidationHasValidSignaturesLabel(value.passedCount), + const Color(0xFF078814), + ), + (_, _, _, _) => ("", Colors.transparent), // technically invalid case + }; + const foregroundColor = Colors.white; + final icon = (hasSignatures ? Icons.arrow_right_alt_outlined : Icons.close); + + final children = [ + if (isLoading) const LoadingIndicator(size: 16), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: MarkdownText(text, textColor: foregroundColor), + ), + ), + Icon( + icon, + color: foregroundColor, + semanticLabel: null, + ) + ]; + + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), + color: backgroundColor, + child: Row( + children: children, + ), + ), + ); + } +} + +/// Value for [DocumentValidationStrip] widget. +class DocumentValidationStripValue { + final bool isLoading; + final int failedCount; + final int indeterminateCount; + final int passedCount; + final Object? error; + + const DocumentValidationStripValue.loading() + : isLoading = true, + failedCount = 0, + indeterminateCount = 0, + passedCount = 0, + error = null; + + const DocumentValidationStripValue.value({ + required this.failedCount, + required this.indeterminateCount, + required this.passedCount, + }) : isLoading = false, + error = null; + + const DocumentValidationStripValue.none() + : this.value( + failedCount: 0, + indeterminateCount: 0, + passedCount: 0, + ); +} + +@widgetbook.UseCase( + path: '[Core]', + name: 'loading', + type: DocumentValidationStrip, +) +Widget previewLoadingDocumentValidationStrip(BuildContext context) { + return const DocumentValidationStrip( + value: DocumentValidationStripValue.loading(), + ); +} + +@widgetbook.UseCase( + path: '[Core]', + name: 'signatures', + type: DocumentValidationStrip, +) +Widget previewOtherDocumentValidationStrip(BuildContext context) { + final passedCount = context.knobs.list( + label: 'Passed count', + options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + initialOption: 0, + ); + final indeterminateCount = context.knobs.list( + label: 'Indeterminate count', + options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + initialOption: 0, + ); + final failedCount = context.knobs.list( + label: 'Failed count', + options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + initialOption: 0, + ); + + return DocumentValidationStrip( + value: DocumentValidationStripValue.value( + failedCount: failedCount, + indeterminateCount: indeterminateCount, + passedCount: passedCount, + ), + ); +} diff --git a/lib/ui/widgets/loading_indicator.dart b/lib/ui/widgets/loading_indicator.dart index 75bcf9b..504434f 100644 --- a/lib/ui/widgets/loading_indicator.dart +++ b/lib/ui/widgets/loading_indicator.dart @@ -3,13 +3,13 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; /// Widget for displaying indeterminate loading indicator. class LoadingIndicator extends StatelessWidget { - static const size = 24; - + final int size; final Color color; final Color backgroundColor; const LoadingIndicator({ super.key, + this.size = 24, this.color = const Color(0xFF126dff), this.backgroundColor = const Color(0xFFc3d9f9), }); diff --git a/lib/ui/widgets/markdown_text.dart b/lib/ui/widgets/markdown_text.dart index 9f957f7..7c9364f 100644 --- a/lib/ui/widgets/markdown_text.dart +++ b/lib/ui/widgets/markdown_text.dart @@ -11,11 +11,13 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; /// By default, links are open in external application. class MarkdownText extends StatelessWidget { final String data; + final Color? textColor; final MarkdownTapLinkCallback onLinkTap; const MarkdownText( this.data, { super.key, + this.textColor, this.onLinkTap = _defaultOnLinkTap, }); @@ -23,6 +25,7 @@ class MarkdownText extends StatelessWidget { Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final styleSheet = MarkdownStyleSheet( + p: (textColor != null ? TextStyle(color: textColor) : null), a: TextStyle( color: colors.primary, fontWeight: FontWeight.bold, diff --git a/lib/ui/widgets/option_picker.dart b/lib/ui/widgets/option_picker.dart index 4a20cd0..130a64a 100644 --- a/lib/ui/widgets/option_picker.dart +++ b/lib/ui/widgets/option_picker.dart @@ -55,18 +55,22 @@ class OptionPicker extends StatelessWidget { ), ); - return Material( - child: InkWell( - onTap: () { - onValueChanged(value); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - radio, - const SizedBox(width: 8), - label, - ], + return Semantics( + checked: value == selectedValue, + inMutuallyExclusiveGroup: true, + child: Material( + child: InkWell( + onTap: () { + onValueChanged(value); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ExcludeSemantics(child: radio), + const SizedBox(width: 8), + label, + ], + ), ), ), ); diff --git a/lib/ui/widgets/signature_type_picker.dart b/lib/ui/widgets/signature_type_picker.dart index fc48990..224a4f5 100644 --- a/lib/ui/widgets/signature_type_picker.dart +++ b/lib/ui/widgets/signature_type_picker.dart @@ -88,18 +88,24 @@ class _ListItem extends StatelessWidget { ); // NOT using RadioListTile because need to scale-up and style Radio - return ListTile( - onTap: (canSelect ? onSelected : null), - enabled: enabled, - leading: Transform.scale( - scale: kRadioScale, - child: radio, + return Semantics( + checked: value == selectedValue, + inMutuallyExclusiveGroup: true, + excludeSemantics: true, + label: "$titleText, $subtitleText", + child: ListTile( + onTap: (canSelect ? onSelected : null), + enabled: enabled, + leading: Transform.scale( + scale: kRadioScale, + child: radio, + ), + title: Text( + titleText, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text(subtitleText), ), - title: Text( - titleText, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text(subtitleText), ); } } diff --git a/lib/widgetbook_app.directories.g.dart b/lib/widgetbook_app.directories.g.dart index 48145ed..59389d6 100644 --- a/lib/widgetbook_app.directories.g.dart +++ b/lib/widgetbook_app.directories.g.dart @@ -11,45 +11,51 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:autogram/ui/app_theme.dart' as _i3; import 'package:autogram/ui/assets.dart' as _i4; -import 'package:autogram/ui/fragment/show_web_page_fragment.dart' as _i22; -import 'package:autogram/ui/screens/about_screen.dart' as _i25; -import 'package:autogram/ui/screens/main_menu_screen.dart' as _i26; +import 'package:autogram/ui/fragment/document_validation_info_fragment.dart' + as _i26; +import 'package:autogram/ui/fragment/preview_document_fragment.dart' as _i27; +import 'package:autogram/ui/fragment/show_web_page_fragment.dart' as _i28; +import 'package:autogram/ui/screens/about_screen.dart' as _i31; +import 'package:autogram/ui/screens/main_menu_screen.dart' as _i32; import 'package:autogram/ui/screens/main_screen.dart' as _i2; import 'package:autogram/ui/screens/onboarding_accept_document_screen.dart' - as _i27; -import 'package:autogram/ui/screens/onboarding_finished_screen.dart' as _i28; + as _i33; +import 'package:autogram/ui/screens/onboarding_finished_screen.dart' as _i34; import 'package:autogram/ui/screens/onboarding_select_signing_certificate_screen.dart' - as _i29; -import 'package:autogram/ui/screens/open_document_screen.dart' as _i30; -import 'package:autogram/ui/screens/paired_device_list_screen.dart' as _i31; + as _i35; +import 'package:autogram/ui/screens/open_document_screen.dart' as _i36; +import 'package:autogram/ui/screens/paired_device_list_screen.dart' as _i37; import 'package:autogram/ui/screens/present_signed_document_screen.dart' - as _i32; -import 'package:autogram/ui/screens/preview_document_screen.dart' as _i33; -import 'package:autogram/ui/screens/qr_code_scanner_screen.dart' as _i20; -import 'package:autogram/ui/screens/select_certificate_screen.dart' as _i34; -import 'package:autogram/ui/screens/settings_screen.dart' as _i35; -import 'package:autogram/ui/screens/show_document_screen.dart' as _i36; -import 'package:autogram/ui/screens/sign_document_screen.dart' as _i37; -import 'package:autogram/ui/screens/start_remote_document_signing_screen.dart' as _i38; -import 'package:autogram/ui/widgets/app_version_text.dart' as _i9; +import 'package:autogram/ui/screens/qr_code_scanner_screen.dart' as _i24; +import 'package:autogram/ui/screens/select_certificate_screen.dart' as _i39; +import 'package:autogram/ui/screens/settings_screen.dart' as _i40; +import 'package:autogram/ui/screens/show_document_screen.dart' as _i41; +import 'package:autogram/ui/screens/sign_document_screen.dart' as _i42; +import 'package:autogram/ui/screens/start_remote_document_signing_screen.dart' + as _i43; +import 'package:autogram/ui/widgets/app_version_text.dart' as _i10; import 'package:autogram/ui/widgets/autogram_logo.dart' as _i5; import 'package:autogram/ui/widgets/buttons.dart' as _i6; -import 'package:autogram/ui/widgets/certificate_picker.dart' as _i23; -import 'package:autogram/ui/widgets/close_button.dart' as _i7; -import 'package:autogram/ui/widgets/dialogs.dart' as _i21; -import 'package:autogram/ui/widgets/document_visualization.dart' as _i10; -import 'package:autogram/ui/widgets/error_content.dart' as _i11; -import 'package:autogram/ui/widgets/html_preview.dart' as _i12; -import 'package:autogram/ui/widgets/loading_content.dart' as _i13; -import 'package:autogram/ui/widgets/loading_indicator.dart' as _i8; -import 'package:autogram/ui/widgets/markdown_text.dart' as _i14; -import 'package:autogram/ui/widgets/option_picker.dart' as _i15; -import 'package:autogram/ui/widgets/preference_tile.dart' as _i16; -import 'package:autogram/ui/widgets/result_view.dart' as _i17; -import 'package:autogram/ui/widgets/retry_view.dart' as _i18; -import 'package:autogram/ui/widgets/signature_type_picker.dart' as _i24; -import 'package:autogram/ui/widgets/step_indicator.dart' as _i19; +import 'package:autogram/ui/widgets/certificate_details.dart' as _i11; +import 'package:autogram/ui/widgets/certificate_picker.dart' as _i29; +import 'package:autogram/ui/widgets/chip.dart' as _i7; +import 'package:autogram/ui/widgets/close_button.dart' as _i8; +import 'package:autogram/ui/widgets/dialogs.dart' as _i25; +import 'package:autogram/ui/widgets/document_signature_info.dart' as _i12; +import 'package:autogram/ui/widgets/document_validation_strip.dart' as _i13; +import 'package:autogram/ui/widgets/document_visualization.dart' as _i14; +import 'package:autogram/ui/widgets/error_content.dart' as _i15; +import 'package:autogram/ui/widgets/html_preview.dart' as _i16; +import 'package:autogram/ui/widgets/loading_content.dart' as _i17; +import 'package:autogram/ui/widgets/loading_indicator.dart' as _i9; +import 'package:autogram/ui/widgets/markdown_text.dart' as _i18; +import 'package:autogram/ui/widgets/option_picker.dart' as _i19; +import 'package:autogram/ui/widgets/preference_tile.dart' as _i20; +import 'package:autogram/ui/widgets/result_view.dart' as _i21; +import 'package:autogram/ui/widgets/retry_view.dart' as _i22; +import 'package:autogram/ui/widgets/signature_type_picker.dart' as _i30; +import 'package:autogram/ui/widgets/step_indicator.dart' as _i23; import 'package:widgetbook/widgetbook.dart' as _i1; final directories = <_i1.WidgetbookNode>[ @@ -102,11 +108,18 @@ final directories = <_i1.WidgetbookNode>[ ), ], ), + _i1.WidgetbookLeafComponent( + name: 'Chip', + useCase: _i1.WidgetbookUseCase( + name: '', + builder: _i7.previewChip, + ), + ), _i1.WidgetbookLeafComponent( name: 'CloseButton', useCase: _i1.WidgetbookUseCase( name: 'CloseButton', - builder: _i7.previewCloseButton, + builder: _i8.previewCloseButton, ), ), _i1.WidgetbookLeafComponent( @@ -120,7 +133,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'LoadingIndicator', useCase: _i1.WidgetbookUseCase( name: 'LoadingIndicator', - builder: _i8.previewLoadingIndicator, + builder: _i9.previewLoadingIndicator, ), ), _i1.WidgetbookLeafComponent( @@ -153,56 +166,83 @@ final directories = <_i1.WidgetbookNode>[ name: 'AppVersionText', useCase: _i1.WidgetbookUseCase( name: '', - builder: _i9.previewAppVersionText, + builder: _i10.previewAppVersionText, ), ), + _i1.WidgetbookLeafComponent( + name: 'CertificateDetails', + useCase: _i1.WidgetbookUseCase( + name: 'CertificateDetails', + builder: _i11.previewCertificateDetails, + ), + ), + _i1.WidgetbookLeafComponent( + name: 'DocumentSignatureInfo', + useCase: _i1.WidgetbookUseCase( + name: '', + builder: _i12.previewDocumentSignatureInfo, + ), + ), + _i1.WidgetbookComponent( + name: 'DocumentValidationStrip', + useCases: [ + _i1.WidgetbookUseCase( + name: 'loading', + builder: _i13.previewLoadingDocumentValidationStrip, + ), + _i1.WidgetbookUseCase( + name: 'signatures', + builder: _i13.previewOtherDocumentValidationStrip, + ), + ], + ), _i1.WidgetbookLeafComponent( name: 'DocumentVisualization', useCase: _i1.WidgetbookUseCase( name: 'DocumentVisualization', - builder: _i10.previewDocumentVisualization, + builder: _i14.previewDocumentVisualization, ), ), _i1.WidgetbookLeafComponent( name: 'ErrorContent', useCase: _i1.WidgetbookUseCase( name: 'ErrorContent', - builder: _i11.previewErrorContent, + builder: _i15.previewErrorContent, ), ), _i1.WidgetbookLeafComponent( name: 'HtmlPreview', useCase: _i1.WidgetbookUseCase( name: 'HtmlPreview', - builder: _i12.previewHtmlPreview, + builder: _i16.previewHtmlPreview, ), ), _i1.WidgetbookLeafComponent( name: 'LoadingContent', useCase: _i1.WidgetbookUseCase( name: 'LoadingContent', - builder: _i13.previewLoadingContent, + builder: _i17.previewLoadingContent, ), ), _i1.WidgetbookLeafComponent( name: 'MarkdownText', useCase: _i1.WidgetbookUseCase( name: '', - builder: _i14.previewMarkdownText, + builder: _i18.previewMarkdownText, ), ), _i1.WidgetbookLeafComponent( name: 'OptionPicker', useCase: _i1.WidgetbookUseCase( name: 'OptionPicker', - builder: _i15.previewOptionPicker, + builder: _i19.previewOptionPicker, ), ), _i1.WidgetbookLeafComponent( name: 'PreferenceTile', useCase: _i1.WidgetbookUseCase( name: 'PreferenceTile', - builder: _i16.previewPreferenceTile, + builder: _i20.previewPreferenceTile, ), ), _i1.WidgetbookComponent( @@ -210,19 +250,19 @@ final directories = <_i1.WidgetbookNode>[ useCases: [ _i1.WidgetbookUseCase( name: 'custom', - builder: _i17.previewCustomResultView, + builder: _i21.previewCustomResultView, ), _i1.WidgetbookUseCase( name: 'error', - builder: _i17.previewErrorResultView, + builder: _i21.previewErrorResultView, ), _i1.WidgetbookUseCase( name: 'info', - builder: _i17.previewInfoResultView, + builder: _i21.previewInfoResultView, ), _i1.WidgetbookUseCase( name: 'success', - builder: _i17.previewSuccessResultView, + builder: _i21.previewSuccessResultView, ), ], ), @@ -230,21 +270,21 @@ final directories = <_i1.WidgetbookNode>[ name: 'RetryView', useCase: _i1.WidgetbookUseCase( name: 'RetryView', - builder: _i18.previewRetryView, + builder: _i22.previewRetryView, ), ), _i1.WidgetbookLeafComponent( name: 'StepIndicator', useCase: _i1.WidgetbookUseCase( name: 'StepIndicator', - builder: _i19.previewStepIndicator, + builder: _i23.previewStepIndicator, ), ), _i1.WidgetbookLeafComponent( name: '_ViewFinder', useCase: _i1.WidgetbookUseCase( name: '', - builder: _i20.previewViewFinder, + builder: _i24.previewViewFinder, ), ), ], @@ -256,7 +296,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'BottomSheet', useCase: _i1.WidgetbookUseCase( name: 'NotificationsPermissionRationale', - builder: _i21.previewNotificationsPermissionRationaleModal, + builder: _i25.previewNotificationsPermissionRationaleModal, ), ) ], @@ -264,13 +304,37 @@ final directories = <_i1.WidgetbookNode>[ _i1.WidgetbookCategory( name: 'Fragments', children: [ + _i1.WidgetbookLeafComponent( + name: 'DocumentValidationInfoFragment', + useCase: _i1.WidgetbookUseCase( + name: '', + builder: _i26.previewDocumentValidationInfoFragment, + ), + ), + _i1.WidgetbookComponent( + name: 'PreviewDocumentFragment', + useCases: [ + _i1.WidgetbookUseCase( + name: 'error', + builder: _i27.previewErrorPreviewDocumentScreen, + ), + _i1.WidgetbookUseCase( + name: 'loading', + builder: _i27.previewLoadingPreviewDocumentFragment, + ), + _i1.WidgetbookUseCase( + name: 'success', + builder: _i27.previewSuccessPreviewDocumentScreen, + ), + ], + ), _i1.WidgetbookLeafComponent( name: 'ShowWebPageFragment', useCase: _i1.WidgetbookUseCase( name: 'ShowWebPageFragment', - builder: _i22.previewShowWebPageFragment, + builder: _i28.previewShowWebPageFragment, ), - ) + ), ], ), _i1.WidgetbookCategory( @@ -280,14 +344,14 @@ final directories = <_i1.WidgetbookNode>[ name: 'CertificatePicker', useCase: _i1.WidgetbookUseCase( name: 'CertificatePicker', - builder: _i23.previewCertificatePicker, + builder: _i29.previewCertificatePicker, ), ), _i1.WidgetbookLeafComponent( name: 'SignatureTypePicker', useCase: _i1.WidgetbookUseCase( name: '', - builder: _i24.previewSignatureTypePicker, + builder: _i30.previewSignatureTypePicker, ), ), ], @@ -299,14 +363,14 @@ final directories = <_i1.WidgetbookNode>[ name: 'AboutScreen', useCase: _i1.WidgetbookUseCase( name: 'AboutScreen', - builder: _i25.previewAboutScreen, + builder: _i31.previewAboutScreen, ), ), _i1.WidgetbookLeafComponent( name: 'MainMenuScreen', useCase: _i1.WidgetbookUseCase( name: '', - builder: _i26.previewMainMenuScreen, + builder: _i32.previewMainMenuScreen, ), ), _i1.WidgetbookLeafComponent( @@ -320,14 +384,14 @@ final directories = <_i1.WidgetbookNode>[ name: 'OnboardingAcceptDocumentScreen', useCase: _i1.WidgetbookUseCase( name: '', - builder: _i27.previewOnboardingAcceptDocumentScreen, + builder: _i33.previewOnboardingAcceptDocumentScreen, ), ), _i1.WidgetbookLeafComponent( name: 'OnboardingFinishedScreen', useCase: _i1.WidgetbookUseCase( name: '', - builder: _i28.previewOnboardingFinishedScreen, + builder: _i34.previewOnboardingFinishedScreen, ), ), _i1.WidgetbookComponent( @@ -335,20 +399,20 @@ final directories = <_i1.WidgetbookNode>[ useCases: [ _i1.WidgetbookUseCase( name: 'canceled', - builder: _i29.previewCanceledOnboardingSelectSigningCertificateBody, + builder: _i35.previewCanceledOnboardingSelectSigningCertificateBody, ), _i1.WidgetbookUseCase( name: 'initial', - builder: _i29.previewInitialOnboardingSelectSigningCertificateBody, + builder: _i35.previewInitialOnboardingSelectSigningCertificateBody, ), _i1.WidgetbookUseCase( name: 'no certificate', builder: - _i29.previewNoCertificateOnboardingSelectSigningCertificateBody, + _i35.previewNoCertificateOnboardingSelectSigningCertificateBody, ), _i1.WidgetbookUseCase( name: 'success', - builder: _i29.previewSuccessOnboardingSelectSigningCertificateBody, + builder: _i35.previewSuccessOnboardingSelectSigningCertificateBody, ), ], ), @@ -357,11 +421,11 @@ final directories = <_i1.WidgetbookNode>[ useCases: [ _i1.WidgetbookUseCase( name: 'error', - builder: _i30.previewErrorOpenDocumentScreen, + builder: _i36.previewErrorOpenDocumentScreen, ), _i1.WidgetbookUseCase( name: 'loading', - builder: _i30.previewLoadingOpenDocumentScreen, + builder: _i36.previewLoadingOpenDocumentScreen, ), ], ), @@ -369,7 +433,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'PairedDeviceListScreen', useCase: _i1.WidgetbookUseCase( name: '', - builder: _i31.previewPairedDeviceListScreen, + builder: _i37.previewPairedDeviceListScreen, ), ), _i1.WidgetbookComponent( @@ -377,36 +441,19 @@ final directories = <_i1.WidgetbookNode>[ useCases: [ _i1.WidgetbookUseCase( name: 'error', - builder: _i32.previewErrorPresentSignedDocumentScreen, + builder: _i38.previewErrorPresentSignedDocumentScreen, ), _i1.WidgetbookUseCase( name: 'initial', - builder: _i32.previewInitialPresentSignedDocumentScreen, - ), - _i1.WidgetbookUseCase( - name: 'loading', - builder: _i32.previewLoadingPresentSignedDocumentScreen, - ), - _i1.WidgetbookUseCase( - name: 'success', - builder: _i32.previewSuccessPresentSignedDocumentScreen, - ), - ], - ), - _i1.WidgetbookComponent( - name: 'PreviewDocumentScreen', - useCases: [ - _i1.WidgetbookUseCase( - name: 'error', - builder: _i33.previewErrorPreviewDocumentScreen, + builder: _i38.previewInitialPresentSignedDocumentScreen, ), _i1.WidgetbookUseCase( name: 'loading', - builder: _i33.previewLoadingPreviewDocumentScreen, + builder: _i38.previewLoadingPresentSignedDocumentScreen, ), _i1.WidgetbookUseCase( name: 'success', - builder: _i33.previewSuccessPreviewDocumentScreen, + builder: _i38.previewSuccessPresentSignedDocumentScreen, ), ], ), @@ -414,7 +461,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'QRCodeScannerScreen', useCase: _i1.WidgetbookUseCase( name: '', - builder: _i20.previewQRCodeScannerScreen, + builder: _i24.previewQRCodeScannerScreen, ), ), _i1.WidgetbookComponent( @@ -422,23 +469,23 @@ final directories = <_i1.WidgetbookNode>[ useCases: [ _i1.WidgetbookUseCase( name: 'canceled', - builder: _i34.previewCanceledSelectCertificateScreen, + builder: _i39.previewCanceledSelectCertificateScreen, ), _i1.WidgetbookUseCase( name: 'error', - builder: _i34.previewErrorSelectCertificateScreen, + builder: _i39.previewErrorSelectCertificateScreen, ), _i1.WidgetbookUseCase( name: 'loading', - builder: _i34.previewLoadingSelectCertificateScreen, + builder: _i39.previewLoadingSelectCertificateScreen, ), _i1.WidgetbookUseCase( name: 'no certificate', - builder: _i34.previewNoCertificateSelectCertificateScreen, + builder: _i39.previewNoCertificateSelectCertificateScreen, ), _i1.WidgetbookUseCase( name: 'success', - builder: _i34.previewSuccessSelectCertificateScreen, + builder: _i39.previewSuccessSelectCertificateScreen, ), ], ), @@ -446,14 +493,14 @@ final directories = <_i1.WidgetbookNode>[ name: 'SettingsScreen', useCase: _i1.WidgetbookUseCase( name: 'SettingsScreen', - builder: _i35.previewSettingsScreen, + builder: _i40.previewSettingsScreen, ), ), _i1.WidgetbookLeafComponent( name: 'ShowDocumentScreen', useCase: _i1.WidgetbookUseCase( name: '', - builder: _i36.previewShowDocumentScreen, + builder: _i41.previewShowDocumentScreen, ), ), _i1.WidgetbookComponent( @@ -461,15 +508,15 @@ final directories = <_i1.WidgetbookNode>[ useCases: [ _i1.WidgetbookUseCase( name: 'error', - builder: _i37.previewErrorSignDocumentScreen, + builder: _i42.previewErrorSignDocumentScreen, ), _i1.WidgetbookUseCase( name: 'loading', - builder: _i37.previewLoadingSignDocumentScreen, + builder: _i42.previewLoadingSignDocumentScreen, ), _i1.WidgetbookUseCase( name: 'success', - builder: _i37.previewSuccessSignDocumentScreen, + builder: _i42.previewSuccessSignDocumentScreen, ), ], ), @@ -477,7 +524,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'StartRemoteDocumentSigningScreen', useCase: _i1.WidgetbookUseCase( name: '', - builder: _i38.previewStartRemoteDocumentSigningScreen, + builder: _i43.previewStartRemoteDocumentSigningScreen, ), ), ], diff --git a/pubspec.lock b/pubspec.lock index 950691e..e2b1a6e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -86,10 +86,10 @@ packages: dependency: transitive description: name: bidi - sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63" + sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.0.12" bloc: dependency: transitive description: @@ -351,10 +351,10 @@ packages: dependency: transitive description: name: firebase_core_platform_interface - sha256: "3c3a1e92d6f4916c32deea79c4a7587aa0e9dbbe5889c7a16afcf005a485ee02" + sha256: f7d7180c7f99babd4b4c517754d41a09a4943a0f7a69b65c894ca5c68ba66315 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.2.1" firebase_core_web: dependency: transitive description: @@ -651,10 +651,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" mobile_scanner: dependency: "direct main" description: @@ -883,10 +883,10 @@ packages: dependency: transitive description: name: qs_dart - sha256: ba7ba5c18f2f0b9356776b4a4641b1166bfe58d681abae75022bfc42cfdb5d1e + sha256: "8dddeaf1d32fe407e253840b2c25c9ab5bf347d2761d82cb4ce010096565c9ff" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.3" recase: dependency: transitive description: @@ -1216,10 +1216,10 @@ packages: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.0" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 22233b7..af90465 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: autogram description: "Autogram v mobile" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.4+35 +version: 1.1.0+36 environment: sdk: '>=3.2.3 <4.0.0'