From 3d69abe2ac1b6eff5b6b5c54747c965f38784490 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matej=20Hlatk=C3=BD?= <hlatky@freevision.sk>
Date: Thu, 27 Jun 2024 09:37:16 +0200
Subject: [PATCH 1/7] Umoznit sharovat subor ak na Android neexistuje Downloads
 (#24)

#18 | Allow sharing signed Document even when failed to save file
---
 CHANGELOG.md                                  |  3 +-
 lib/bloc/present_signed_document_cubit.dart   | 43 ++++++++++++-------
 lib/pages/test_page.dart                      |  1 +
 .../present_signed_document_screen.dart       | 42 ++++++++++++++----
 lib/ui/screens/sign_document_screen.dart      | 16 +++++--
 pubspec.lock                                  |  4 +-
 6 files changed, 79 insertions(+), 30 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6c10a1..1240dcc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,8 @@
 
 ## NEXT - v1.0.2(33)
  
-- Refactor Settings model 
+- Refactor Settings model
+- #18 | Allow sharing signed Document even when failed to save file
 
 ## 2024-06-17 - v1.0.1(1)
  
diff --git a/lib/bloc/present_signed_document_cubit.dart b/lib/bloc/present_signed_document_cubit.dart
index 02dd557..550821c 100644
--- a/lib/bloc/present_signed_document_cubit.dart
+++ b/lib/bloc/present_signed_document_cubit.dart
@@ -1,5 +1,5 @@
 import 'dart:convert' show base64Decode;
-import 'dart:io' show File;
+import 'dart:io' show File, Platform;
 
 import 'package:autogram_sign/autogram_sign.dart' show SignDocumentResponseBody;
 import 'package:flutter/foundation.dart';
@@ -18,6 +18,9 @@ import 'present_signed_document_state.dart';
 export 'present_signed_document_state.dart';
 
 /// Cubit for [PresentSignedDocumentScreen].
+///
+/// Allows saving document into public directory or getting [File] instance
+/// which can be shared.
 @injectable
 class PresentSignedDocumentCubit extends Cubit<PresentSignedDocumentState> {
   static final _log = Logger("PresentSignedDocumentCubit");
@@ -33,6 +36,7 @@ class PresentSignedDocumentCubit extends Cubit<PresentSignedDocumentState> {
   })  : _appService = appService,
         super(const PresentSignedDocumentInitialState());
 
+  /// Saves [signedDocument] into public directory.
   Future<void> saveDocument() async {
     _log.info("Saving signed document: ${signedDocument.filename}.");
 
@@ -41,11 +45,10 @@ class PresentSignedDocumentCubit extends Cubit<PresentSignedDocumentState> {
     File? file;
 
     try {
-      file = await _getTargetFile();
-      final bytes = await Future.microtask(
-        () => base64Decode(signedDocument.content),
-      );
-      await file.writeAsBytes(bytes);
+      file = await _getTargetPath().then((path) => File(path));
+      // TODO Catch and still allow sharing
+      // Need to change PresentSignedDocumentSuccessState impl. to allow File?
+      await _saveDocumentIntoFile(file!);
 
       _log.info("Signed Document was saved into $file");
 
@@ -73,24 +76,34 @@ class PresentSignedDocumentCubit extends Cubit<PresentSignedDocumentState> {
     final name = signedDocument.filename;
     final directory = await getTemporaryDirectory();
     final path = p.join(directory.path, name);
-    final bytes = await Future.microtask(
-      () => base64Decode(signedDocument.content),
-    );
     final file = File(path);
 
-    return file.writeAsBytes(bytes, flush: true);
+    await _saveDocumentIntoFile(file);
+
+    return file;
   }
 
-  /// Returns target [File] where to save new file from [signedDocument].
+  /// Returns file path, where [signedDocument] content should be saved.
   ///
   /// See also:
   ///  - [getTargetFileName]
-  Future<File> _getTargetFile() async {
+  Future<String> _getTargetPath() async {
     final directory = await _appService.getDocumentsDirectory();
+
+    // Attempt to create Directory if not exists
+    if (!(await directory.exists()) && Platform.isAndroid) {
+      await directory.create(recursive: true);
+    }
+
     final name = getTargetFileName(signedDocument.filename);
-    final path = p.join(directory.path, name);
 
-    return File(path);
+    return p.join(directory.path, name);
+  }
+
+  /// Saves [signedDocument] content into given [file].
+  Future<void> _saveDocumentIntoFile(File file) {
+    return Future.microtask(() => base64Decode(signedDocument.content))
+        .then((bytes) => file.writeAsBytes(bytes, flush: true));
   }
 
   /// Gets the target file name.
@@ -98,7 +111,7 @@ class PresentSignedDocumentCubit extends Cubit<PresentSignedDocumentState> {
   @visibleForTesting
   static String getTargetFileName(
     String name, [
-    // TODO This should get exact DateTime from previous cubit when it was really signed
+    // TODO This should get exact DateTime from previous cubit when it was actually signed
     // SignDocumentCubit signingTime
     ValueGetter<DateTime> clock = DateTime.now,
   ]) {
diff --git a/lib/pages/test_page.dart b/lib/pages/test_page.dart
index 7e5caf2..47a97a7 100644
--- a/lib/pages/test_page.dart
+++ b/lib/pages/test_page.dart
@@ -15,6 +15,7 @@ import 'package:share_plus/share_plus.dart';
 
 import '../utils.dart' as utils;
 
+// TODO Move to ui/screens, add some docs
 class TestPage extends HookWidget {
   const TestPage({super.key});
 
diff --git a/lib/ui/screens/present_signed_document_screen.dart b/lib/ui/screens/present_signed_document_screen.dart
index 6142082..777b8d9 100644
--- a/lib/ui/screens/present_signed_document_screen.dart
+++ b/lib/ui/screens/present_signed_document_screen.dart
@@ -1,3 +1,4 @@
+import 'dart:developer' as developer;
 import 'dart:io' show File, OSError, PathAccessException;
 
 import 'package:autogram_sign/autogram_sign.dart' show SignDocumentResponseBody;
@@ -20,6 +21,9 @@ import '../widgets/result_view.dart';
 
 /// Screen for presenting signed document.
 ///
+/// When [signingType] is [DocumentSigningType.local], then document is saved
+/// into this device and also "Share" button is visible.
+///
 /// Uses [PresentSignedDocumentCubit].
 class PresentSignedDocumentScreen extends StatelessWidget {
   final SignDocumentResponseBody signedDocument;
@@ -144,6 +148,10 @@ class _Body extends StatelessWidget {
   }
 
   Widget _getChild(BuildContext context) {
+    final sharingEnabled = (signingType == DocumentSigningType.local);
+    final onShareFileRequested =
+        sharingEnabled ? this.onShareFileRequested : null;
+
     return switch (state) {
       PresentSignedDocumentInitialState _ => _SuccessContent(
           file: null,
@@ -153,7 +161,7 @@ class _Body extends StatelessWidget {
       PresentSignedDocumentLoadingState _ => const LoadingContent(),
       PresentSignedDocumentErrorState _ => _SuccessContent(
           file: null,
-          onShareFileRequested: null,
+          onShareFileRequested: onShareFileRequested,
           onCloseRequested: onCloseRequested,
         ),
       PresentSignedDocumentSuccessState state => _SuccessContent(
@@ -257,7 +265,12 @@ Widget previewInitialPresentSignedDocumentScreen(BuildContext context) {
   return _Body(
     state: const PresentSignedDocumentInitialState(),
     signingType: signingType,
-    onCloseRequested: () {},
+    onShareFileRequested: () {
+      developer.log('onShareFileRequested');
+    },
+    onCloseRequested: () {
+      developer.log('onCloseRequested');
+    },
   );
 }
 
@@ -276,6 +289,12 @@ Widget previewLoadingPresentSignedDocumentScreen(BuildContext context) {
   return _Body(
     state: const PresentSignedDocumentLoadingState(),
     signingType: signingType,
+    onShareFileRequested: () {
+      developer.log('onShareFileRequested');
+    },
+    onCloseRequested: () {
+      developer.log('onCloseRequested');
+    },
   );
 }
 
@@ -291,7 +310,7 @@ Widget previewErrorPresentSignedDocumentScreen(BuildContext context) {
     initialOption: DocumentSigningType.local,
   );
 
-  // TODO Should preview whole Screen class also with BlocConsumer.listener
+  // TODO Should preview whole Screen class also with BlocConsumer.listener to display error in SnackBar
   const error = PathAccessException(
     "/storage/emulated/0/Download/container-signed-xades-baseline-b.sce",
     OSError("Permission denied", 13),
@@ -301,8 +320,12 @@ Widget previewErrorPresentSignedDocumentScreen(BuildContext context) {
   return _Body(
     state: const PresentSignedDocumentErrorState(error),
     signingType: signingType,
-    onShareFileRequested: () {},
-    onCloseRequested: () {},
+    onShareFileRequested: () {
+      developer.log('onShareFileRequested');
+    },
+    onCloseRequested: () {
+      developer.log('onCloseRequested');
+    },
   );
 }
 
@@ -326,8 +349,11 @@ Widget previewSuccessPresentSignedDocumentScreen(BuildContext context) {
   return _Body(
     state: PresentSignedDocumentSuccessState(file),
     signingType: signingType,
-    onShareFileRequested:
-        signingType == DocumentSigningType.local ? () {} : null,
-    onCloseRequested: () {},
+    onShareFileRequested: () {
+      developer.log('onShareFileRequested');
+    },
+    onCloseRequested: () {
+      developer.log('onCloseRequested');
+    },
   );
 }
diff --git a/lib/ui/screens/sign_document_screen.dart b/lib/ui/screens/sign_document_screen.dart
index 1f4e8e7..9a08055 100644
--- a/lib/ui/screens/sign_document_screen.dart
+++ b/lib/ui/screens/sign_document_screen.dart
@@ -1,3 +1,5 @@
+import 'dart:developer' as developer;
+
 import 'package:autogram_sign/autogram_sign.dart'
     show SignDocumentResponseBody, SignDocumentResponseBodyMimeType;
 import 'package:eidmsdk/types.dart' show Certificate;
@@ -141,8 +143,11 @@ Widget previewLoadingSignDocumentScreen(BuildContext context) {
   type: SignDocumentScreen,
 )
 Widget previewErrorSignDocumentScreen(BuildContext context) {
-  return const _Body(
-    state: SignDocumentErrorState("Error message!"),
+  return _Body(
+    state: const SignDocumentErrorState("Error message!"),
+    onRetryRequested: () {
+      developer.log('onRetryRequested');
+    },
   );
 }
 
@@ -152,8 +157,8 @@ Widget previewErrorSignDocumentScreen(BuildContext context) {
   type: SignDocumentScreen,
 )
 Widget previewSuccessSignDocumentScreen(BuildContext context) {
-  return const _Body(
-    state: SignDocumentSuccessState(
+  return _Body(
+    state: const SignDocumentSuccessState(
       SignDocumentResponseBody(
         filename: "document.pdf",
         mimeType: SignDocumentResponseBodyMimeType.applicationPdfBase64,
@@ -162,5 +167,8 @@ Widget previewSuccessSignDocumentScreen(BuildContext context) {
         signedBy: "",
       ),
     ),
+    onRetryRequested: () {
+      developer.log('onRetryRequested');
+    },
   );
 }
diff --git a/pubspec.lock b/pubspec.lock
index 59de53e..1ef81f4 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -767,10 +767,10 @@ packages:
     dependency: "direct main"
     description:
       name: pdf
-      sha256: "243f05342fc0bdf140eba5b069398985cdbdd3dbb1d776cf43d5ea29cc570ba6"
+      sha256: "81d5522bddc1ef5c28e8f0ee40b71708761753c163e0c93a40df56fd515ea0f0"
       url: "https://pub.dev"
     source: hosted
-    version: "3.10.8"
+    version: "3.11.0"
   pdf_widget_wrapper:
     dependency: transitive
     description:

From c22c57e617dfc6074c69cf2cafe9e6bfd8a3c85c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matej=20Hlatk=C3=BD?= <hlatky@freevision.sk>
Date: Sun, 30 Jun 2024 17:36:29 +0200
Subject: [PATCH 2/7] About Screen upravy (#26)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Fix typo

* Using markdown text with links

* Using markdown in about texts

* Prepare MarkdownText widget

* Using MarkdownText on AboutScreen

* Ad "utm_source" into links

* Update TODO

* Fix vertical content overflow

* Document changes

* change ssd url

---------

Co-authored-by: Marek Ceľuch <celuchmarek@gmail.com>
---
 CHANGELOG.md                          |   2 +
 lib/l10n/app_localizations.dart       |   4 +-
 lib/l10n/app_localizations_sk.dart    |   4 +-
 lib/l10n/app_sk.arb                   |   4 +-
 lib/ui/screens/about_screen.dart      |  31 ++++--
 lib/ui/widgets/markdown_text.dart     |  82 +++++++++++++++
 lib/widgetbook_app.directories.g.dart | 142 ++++++++++++++------------
 pubspec.lock                          |  16 +++
 pubspec.yaml                          |   1 +
 9 files changed, 207 insertions(+), 79 deletions(-)
 create mode 100644 lib/ui/widgets/markdown_text.dart

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1240dcc..14c8076 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
  
 - Refactor Settings model
 - #18 | Allow sharing signed Document even when failed to save file
+- About screen - add links into "about" text
+- About screen - fix vertical content overflow on smaller screens
 
 ## 2024-06-17 - v1.0.1(1)
  
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index cf01787..b6f8a2f 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -352,13 +352,13 @@ abstract class AppLocalizations {
   /// No description provided for @eidSDKLicenseText.
   ///
   /// In sk, this message translates to:
-  /// **'Na komunikáciu s čipom občianskeho preukazu je použitá knižnica eID mSDK od Ministerstva vnútra Slovenskej republiky. Knižnica eID mSDK a podmienky jej použitia sú zverejnené na stránke „https://github.com/eidmsdk“'**
+  /// **'Na komunikáciu s čipom občianskeho preukazu je použitá knižnica eID mSDK od Ministerstva vnútra Slovenskej republiky. Knižnica eID mSDK a podmienky jej použitia sú zverejnené na stránke „<https://github.com/eidmsdk>“.'**
   String get eidSDKLicenseText;
 
   /// No description provided for @aboutAuthorsText.
   ///
   /// In sk, this message translates to:
-  /// **'Autormi tohto projektu sú freevision s.r.o., Služby Slovensko.Digital, s.r.o. a ďalší dobrovoľníci. Prevádzku zabezpečuje Služby Slovensko.Digital, s.r.o. Zdrojové kódy sú dpstupné na GitHub-e organizácie Slovensko.Digital.'**
+  /// **'Autormi tohto projektu sú [freevision s.r.o.](https://freevision.sk/?utm_source=digital.slovensko.avm), [Služby Slovensko.Digital, s.r.o.](https://ekosystem.slovensko.digital/?utm_source=digital.slovensko.avm) a ďalší dobrovoľníci. Prevádzku zabezpečuje [Služby Slovensko.Digital, s.r.o.](https://ekosystem.slovensko.digital/?utm_source=digital.slovensko.avm)\n\nZdrojové kódy sú dostupné na [GitHub-e organizácie Slovensko.Digital](https://github.com/slovensko-digital/avm-app-flutter).'**
   String get aboutAuthorsText;
 
   /// No description provided for @thirdPartyLicensesLabel.
diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart
index 82acb06..43ff162 100644
--- a/lib/l10n/app_localizations_sk.dart
+++ b/lib/l10n/app_localizations_sk.dart
@@ -182,10 +182,10 @@ class AppLocalizationsSk extends AppLocalizations {
   String get aboutTitle => 'O aplikácii';
 
   @override
-  String get eidSDKLicenseText => 'Na komunikáciu s čipom občianskeho preukazu je použitá knižnica eID mSDK od Ministerstva vnútra Slovenskej republiky. Knižnica eID mSDK a podmienky jej použitia sú zverejnené na stránke „https://github.com/eidmsdk“';
+  String get eidSDKLicenseText => 'Na komunikáciu s čipom občianskeho preukazu je použitá knižnica eID mSDK od Ministerstva vnútra Slovenskej republiky. Knižnica eID mSDK a podmienky jej použitia sú zverejnené na stránke „<https://github.com/eidmsdk>“.';
 
   @override
-  String get aboutAuthorsText => 'Autormi tohto projektu sú freevision s.r.o., Služby Slovensko.Digital, s.r.o. a ďalší dobrovoľníci. Prevádzku zabezpečuje Služby Slovensko.Digital, s.r.o. Zdrojové kódy sú dpstupné na GitHub-e organizácie Slovensko.Digital.';
+  String get aboutAuthorsText => 'Autormi tohto projektu sú [freevision s.r.o.](https://freevision.sk/?utm_source=digital.slovensko.avm), [Služby Slovensko.Digital, s.r.o.](https://ekosystem.slovensko.digital/?utm_source=digital.slovensko.avm) a ďalší dobrovoľníci. Prevádzku zabezpečuje [Služby Slovensko.Digital, s.r.o.](https://ekosystem.slovensko.digital/?utm_source=digital.slovensko.avm)\n\nZdrojové kódy sú dostupné na [GitHub-e organizácie Slovensko.Digital](https://github.com/slovensko-digital/avm-app-flutter).';
 
   @override
   String get thirdPartyLicensesLabel => 'Licencie knižníc tretích strán';
diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb
index 29870f6..17b2485 100644
--- a/lib/l10n/app_sk.arb
+++ b/lib/l10n/app_sk.arb
@@ -48,8 +48,8 @@
   "termsOfServiceUrl": "https://sluzby.slovensko.digital/autogram-v-mobile/vseobecne-obchodne-podmienky",
 
   "aboutTitle": "O aplikácii",
-  "eidSDKLicenseText": "Na komunikáciu s čipom občianskeho preukazu je použitá knižnica eID mSDK od Ministerstva vnútra Slovenskej republiky. Knižnica eID mSDK a podmienky jej použitia sú zverejnené na\u00A0stránke „https://github.com/eidmsdk“",
-  "aboutAuthorsText": "Autormi tohto projektu sú freevision\u00A0s.r.o., Služby\u00A0Slovensko.Digital,\u00A0s.r.o. a ďalší dobrovoľníci. Prevádzku zabezpečuje Služby\u00A0Slovensko.Digital,\u00A0s.r.o. Zdrojové kódy sú dpstupné na GitHub-e organizácie Slovensko.Digital.",
+  "eidSDKLicenseText": "Na komunikáciu s čipom občianskeho preukazu je použitá knižnica eID mSDK od Ministerstva vnútra Slovenskej republiky. Knižnica eID mSDK a podmienky jej použitia sú zverejnené na\u00A0stránke „<https://github.com/eidmsdk>“.",
+  "aboutAuthorsText": "Autormi tohto projektu sú [freevision\u00A0s.r.o.](https://freevision.sk/?utm_source=digital.slovensko.avm), [Služby\u00A0Slovensko.Digital,\u00A0s.r.o.](https://ekosystem.slovensko.digital/?utm_source=digital.slovensko.avm) a ďalší dobrovoľníci. Prevádzku zabezpečuje [Služby\u00A0Slovensko.Digital,\u00A0s.r.o.](https://ekosystem.slovensko.digital/?utm_source=digital.slovensko.avm)\n\nZdrojové kódy sú dostupné na [GitHub-e organizácie Slovensko.Digital](https://github.com/slovensko-digital/avm-app-flutter).",
   "thirdPartyLicensesLabel": "Licencie knižníc tretích strán",
 
   "introHeading": "Nový, lepší a krajší podpisovač v mobile",
diff --git a/lib/ui/screens/about_screen.dart b/lib/ui/screens/about_screen.dart
index 08e5e8b..a39a645 100644
--- a/lib/ui/screens/about_screen.dart
+++ b/lib/ui/screens/about_screen.dart
@@ -4,9 +4,13 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
 import '../../strings_context.dart';
 import '../app_theme.dart';
 import '../widgets/app_version_text.dart';
+import '../widgets/markdown_text.dart';
 import 'show_document_screen.dart';
 
-/// Displays About.
+/// Displays About appliaction:
+///  - headline, version
+///  - authors, eID mSDK info
+///  - link to [showLicensePage]
 ///
 /// See also:
 ///  - [ShowDocumentScreen]
@@ -44,9 +48,10 @@ class _Body extends StatelessWidget {
         const SizedBox(height: 16),
         const AppVersionText(),
         const SizedBox(height: 16),
-        Text(strings.aboutAuthorsText),
+        MarkdownText(strings.aboutAuthorsText),
         const SizedBox(height: 16),
-        Text(strings.eidSDKLicenseText),
+        MarkdownText(strings.eidSDKLicenseText),
+        const SizedBox(height: kButtonSpace),
         const Spacer(),
         TextButton(
           style: TextButton.styleFrom(
@@ -58,9 +63,23 @@ class _Body extends StatelessWidget {
       ],
     );
 
-    return Padding(
-      padding: kScreenMargin,
-      child: child,
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        return SingleChildScrollView(
+          child: ConstrainedBox(
+            constraints: BoxConstraints(
+              minWidth: constraints.maxWidth,
+              minHeight: constraints.maxHeight,
+            ),
+            child: IntrinsicHeight(
+              child: Padding(
+                padding: kScreenMargin,
+                child: child,
+              ),
+            ),
+          ),
+        );
+      },
     );
   }
 
diff --git a/lib/ui/widgets/markdown_text.dart b/lib/ui/widgets/markdown_text.dart
new file mode 100644
index 0000000..9f957f7
--- /dev/null
+++ b/lib/ui/widgets/markdown_text.dart
@@ -0,0 +1,82 @@
+import 'dart:developer' as developer;
+
+import 'package:flutter/material.dart';
+import 'package:flutter_markdown/flutter_markdown.dart';
+import 'package:url_launcher/url_launcher_string.dart';
+import 'package:widgetbook/widgetbook.dart';
+import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
+
+/// Displays text formatted in Markdown.
+///
+/// By default, links are open in external application.
+class MarkdownText extends StatelessWidget {
+  final String data;
+  final MarkdownTapLinkCallback onLinkTap;
+
+  const MarkdownText(
+    this.data, {
+    super.key,
+    this.onLinkTap = _defaultOnLinkTap,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final colors = Theme.of(context).colorScheme;
+    final styleSheet = MarkdownStyleSheet(
+      a: TextStyle(
+        color: colors.primary,
+        fontWeight: FontWeight.bold,
+        //decoration: TextDecoration.underline, // UGLY :/
+        decorationColor: colors.primary,
+      ),
+    );
+
+    return MarkdownBody(
+      data: data,
+      styleSheet: styleSheet,
+      onTapLink: onLinkTap,
+    );
+  }
+
+  static void _defaultOnLinkTap(String text, String? href, String title) {
+    if (href != null) {
+      launchUrlString(href, mode: LaunchMode.externalApplication);
+    }
+  }
+}
+
+@widgetbook.UseCase(
+  path: '[Core]',
+  name: '',
+  type: MarkdownText,
+)
+Widget previewMarkdownText(BuildContext context) {
+  final data = context.knobs.string(
+    label: 'Data',
+    initialValue: """
+# Supported Markdown
+
+## Unordered List + Text formatting
+
+- This is normal text
+- **This is bold text**
+- _This is italic text_
+- ~~This is striked text~~
+- [Link text](https://slovensko.digital)
+
+## Ordered List
+
+1. Lorem ipsum dolor sit amet
+2. Consectetur adipiscing elit
+3. Integer molestie lorem at massa
+     """,
+    maxLines: 5,
+  );
+
+  return MarkdownText(
+    data,
+    onLinkTap: (String text, String? href, String title) {
+      developer.log("On link tap: $text -> $href");
+    },
+  );
+}
diff --git a/lib/widgetbook_app.directories.g.dart b/lib/widgetbook_app.directories.g.dart
index c752a52..48145ed 100644
--- a/lib/widgetbook_app.directories.g.dart
+++ b/lib/widgetbook_app.directories.g.dart
@@ -11,44 +11,45 @@
 // 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 _i21;
-import 'package:autogram/ui/screens/about_screen.dart' as _i24;
-import 'package:autogram/ui/screens/main_menu_screen.dart' as _i25;
+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/screens/main_screen.dart' as _i2;
 import 'package:autogram/ui/screens/onboarding_accept_document_screen.dart'
-    as _i26;
-import 'package:autogram/ui/screens/onboarding_finished_screen.dart' as _i27;
+    as _i27;
+import 'package:autogram/ui/screens/onboarding_finished_screen.dart' as _i28;
 import 'package:autogram/ui/screens/onboarding_select_signing_certificate_screen.dart'
-    as _i28;
-import 'package:autogram/ui/screens/open_document_screen.dart' as _i29;
-import 'package:autogram/ui/screens/paired_device_list_screen.dart' as _i30;
+    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;
 import 'package:autogram/ui/screens/present_signed_document_screen.dart'
-    as _i31;
-import 'package:autogram/ui/screens/preview_document_screen.dart' as _i32;
-import 'package:autogram/ui/screens/qr_code_scanner_screen.dart' as _i19;
-import 'package:autogram/ui/screens/select_certificate_screen.dart' as _i33;
-import 'package:autogram/ui/screens/settings_screen.dart' as _i34;
-import 'package:autogram/ui/screens/show_document_screen.dart' as _i35;
-import 'package:autogram/ui/screens/sign_document_screen.dart' as _i36;
+    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 _i37;
+    as _i38;
 import 'package:autogram/ui/widgets/app_version_text.dart' as _i9;
 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 _i22;
+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 _i20;
+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/option_picker.dart' as _i14;
-import 'package:autogram/ui/widgets/preference_tile.dart' as _i15;
-import 'package:autogram/ui/widgets/result_view.dart' as _i16;
-import 'package:autogram/ui/widgets/retry_view.dart' as _i17;
-import 'package:autogram/ui/widgets/signature_type_picker.dart' as _i23;
-import 'package:autogram/ui/widgets/step_indicator.dart' as _i18;
+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:widgetbook/widgetbook.dart' as _i1;
 
 final directories = <_i1.WidgetbookNode>[
@@ -183,18 +184,25 @@ final directories = <_i1.WidgetbookNode>[
           builder: _i13.previewLoadingContent,
         ),
       ),
+      _i1.WidgetbookLeafComponent(
+        name: 'MarkdownText',
+        useCase: _i1.WidgetbookUseCase(
+          name: '',
+          builder: _i14.previewMarkdownText,
+        ),
+      ),
       _i1.WidgetbookLeafComponent(
         name: 'OptionPicker',
         useCase: _i1.WidgetbookUseCase(
           name: 'OptionPicker',
-          builder: _i14.previewOptionPicker,
+          builder: _i15.previewOptionPicker,
         ),
       ),
       _i1.WidgetbookLeafComponent(
         name: 'PreferenceTile',
         useCase: _i1.WidgetbookUseCase(
           name: 'PreferenceTile',
-          builder: _i15.previewPreferenceTile,
+          builder: _i16.previewPreferenceTile,
         ),
       ),
       _i1.WidgetbookComponent(
@@ -202,19 +210,19 @@ final directories = <_i1.WidgetbookNode>[
         useCases: [
           _i1.WidgetbookUseCase(
             name: 'custom',
-            builder: _i16.previewCustomResultView,
+            builder: _i17.previewCustomResultView,
           ),
           _i1.WidgetbookUseCase(
             name: 'error',
-            builder: _i16.previewErrorResultView,
+            builder: _i17.previewErrorResultView,
           ),
           _i1.WidgetbookUseCase(
             name: 'info',
-            builder: _i16.previewInfoResultView,
+            builder: _i17.previewInfoResultView,
           ),
           _i1.WidgetbookUseCase(
             name: 'success',
-            builder: _i16.previewSuccessResultView,
+            builder: _i17.previewSuccessResultView,
           ),
         ],
       ),
@@ -222,21 +230,21 @@ final directories = <_i1.WidgetbookNode>[
         name: 'RetryView',
         useCase: _i1.WidgetbookUseCase(
           name: 'RetryView',
-          builder: _i17.previewRetryView,
+          builder: _i18.previewRetryView,
         ),
       ),
       _i1.WidgetbookLeafComponent(
         name: 'StepIndicator',
         useCase: _i1.WidgetbookUseCase(
           name: 'StepIndicator',
-          builder: _i18.previewStepIndicator,
+          builder: _i19.previewStepIndicator,
         ),
       ),
       _i1.WidgetbookLeafComponent(
         name: '_ViewFinder',
         useCase: _i1.WidgetbookUseCase(
           name: '',
-          builder: _i19.previewViewFinder,
+          builder: _i20.previewViewFinder,
         ),
       ),
     ],
@@ -248,7 +256,7 @@ final directories = <_i1.WidgetbookNode>[
         name: 'BottomSheet',
         useCase: _i1.WidgetbookUseCase(
           name: 'NotificationsPermissionRationale',
-          builder: _i20.previewNotificationsPermissionRationaleModal,
+          builder: _i21.previewNotificationsPermissionRationaleModal,
         ),
       )
     ],
@@ -260,7 +268,7 @@ final directories = <_i1.WidgetbookNode>[
         name: 'ShowWebPageFragment',
         useCase: _i1.WidgetbookUseCase(
           name: 'ShowWebPageFragment',
-          builder: _i21.previewShowWebPageFragment,
+          builder: _i22.previewShowWebPageFragment,
         ),
       )
     ],
@@ -272,14 +280,14 @@ final directories = <_i1.WidgetbookNode>[
         name: 'CertificatePicker',
         useCase: _i1.WidgetbookUseCase(
           name: 'CertificatePicker',
-          builder: _i22.previewCertificatePicker,
+          builder: _i23.previewCertificatePicker,
         ),
       ),
       _i1.WidgetbookLeafComponent(
         name: 'SignatureTypePicker',
         useCase: _i1.WidgetbookUseCase(
           name: '',
-          builder: _i23.previewSignatureTypePicker,
+          builder: _i24.previewSignatureTypePicker,
         ),
       ),
     ],
@@ -291,14 +299,14 @@ final directories = <_i1.WidgetbookNode>[
         name: 'AboutScreen',
         useCase: _i1.WidgetbookUseCase(
           name: 'AboutScreen',
-          builder: _i24.previewAboutScreen,
+          builder: _i25.previewAboutScreen,
         ),
       ),
       _i1.WidgetbookLeafComponent(
         name: 'MainMenuScreen',
         useCase: _i1.WidgetbookUseCase(
           name: '',
-          builder: _i25.previewMainMenuScreen,
+          builder: _i26.previewMainMenuScreen,
         ),
       ),
       _i1.WidgetbookLeafComponent(
@@ -312,14 +320,14 @@ final directories = <_i1.WidgetbookNode>[
         name: 'OnboardingAcceptDocumentScreen',
         useCase: _i1.WidgetbookUseCase(
           name: '',
-          builder: _i26.previewOnboardingAcceptDocumentScreen,
+          builder: _i27.previewOnboardingAcceptDocumentScreen,
         ),
       ),
       _i1.WidgetbookLeafComponent(
         name: 'OnboardingFinishedScreen',
         useCase: _i1.WidgetbookUseCase(
           name: '',
-          builder: _i27.previewOnboardingFinishedScreen,
+          builder: _i28.previewOnboardingFinishedScreen,
         ),
       ),
       _i1.WidgetbookComponent(
@@ -327,20 +335,20 @@ final directories = <_i1.WidgetbookNode>[
         useCases: [
           _i1.WidgetbookUseCase(
             name: 'canceled',
-            builder: _i28.previewCanceledOnboardingSelectSigningCertificateBody,
+            builder: _i29.previewCanceledOnboardingSelectSigningCertificateBody,
           ),
           _i1.WidgetbookUseCase(
             name: 'initial',
-            builder: _i28.previewInitialOnboardingSelectSigningCertificateBody,
+            builder: _i29.previewInitialOnboardingSelectSigningCertificateBody,
           ),
           _i1.WidgetbookUseCase(
             name: 'no certificate',
             builder:
-                _i28.previewNoCertificateOnboardingSelectSigningCertificateBody,
+                _i29.previewNoCertificateOnboardingSelectSigningCertificateBody,
           ),
           _i1.WidgetbookUseCase(
             name: 'success',
-            builder: _i28.previewSuccessOnboardingSelectSigningCertificateBody,
+            builder: _i29.previewSuccessOnboardingSelectSigningCertificateBody,
           ),
         ],
       ),
@@ -349,11 +357,11 @@ final directories = <_i1.WidgetbookNode>[
         useCases: [
           _i1.WidgetbookUseCase(
             name: 'error',
-            builder: _i29.previewErrorOpenDocumentScreen,
+            builder: _i30.previewErrorOpenDocumentScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'loading',
-            builder: _i29.previewLoadingOpenDocumentScreen,
+            builder: _i30.previewLoadingOpenDocumentScreen,
           ),
         ],
       ),
@@ -361,7 +369,7 @@ final directories = <_i1.WidgetbookNode>[
         name: 'PairedDeviceListScreen',
         useCase: _i1.WidgetbookUseCase(
           name: '',
-          builder: _i30.previewPairedDeviceListScreen,
+          builder: _i31.previewPairedDeviceListScreen,
         ),
       ),
       _i1.WidgetbookComponent(
@@ -369,19 +377,19 @@ final directories = <_i1.WidgetbookNode>[
         useCases: [
           _i1.WidgetbookUseCase(
             name: 'error',
-            builder: _i31.previewErrorPresentSignedDocumentScreen,
+            builder: _i32.previewErrorPresentSignedDocumentScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'initial',
-            builder: _i31.previewInitialPresentSignedDocumentScreen,
+            builder: _i32.previewInitialPresentSignedDocumentScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'loading',
-            builder: _i31.previewLoadingPresentSignedDocumentScreen,
+            builder: _i32.previewLoadingPresentSignedDocumentScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'success',
-            builder: _i31.previewSuccessPresentSignedDocumentScreen,
+            builder: _i32.previewSuccessPresentSignedDocumentScreen,
           ),
         ],
       ),
@@ -390,15 +398,15 @@ final directories = <_i1.WidgetbookNode>[
         useCases: [
           _i1.WidgetbookUseCase(
             name: 'error',
-            builder: _i32.previewErrorPreviewDocumentScreen,
+            builder: _i33.previewErrorPreviewDocumentScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'loading',
-            builder: _i32.previewLoadingPreviewDocumentScreen,
+            builder: _i33.previewLoadingPreviewDocumentScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'success',
-            builder: _i32.previewSuccessPreviewDocumentScreen,
+            builder: _i33.previewSuccessPreviewDocumentScreen,
           ),
         ],
       ),
@@ -406,7 +414,7 @@ final directories = <_i1.WidgetbookNode>[
         name: 'QRCodeScannerScreen',
         useCase: _i1.WidgetbookUseCase(
           name: '',
-          builder: _i19.previewQRCodeScannerScreen,
+          builder: _i20.previewQRCodeScannerScreen,
         ),
       ),
       _i1.WidgetbookComponent(
@@ -414,23 +422,23 @@ final directories = <_i1.WidgetbookNode>[
         useCases: [
           _i1.WidgetbookUseCase(
             name: 'canceled',
-            builder: _i33.previewCanceledSelectCertificateScreen,
+            builder: _i34.previewCanceledSelectCertificateScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'error',
-            builder: _i33.previewErrorSelectCertificateScreen,
+            builder: _i34.previewErrorSelectCertificateScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'loading',
-            builder: _i33.previewLoadingSelectCertificateScreen,
+            builder: _i34.previewLoadingSelectCertificateScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'no certificate',
-            builder: _i33.previewNoCertificateSelectCertificateScreen,
+            builder: _i34.previewNoCertificateSelectCertificateScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'success',
-            builder: _i33.previewSuccessSelectCertificateScreen,
+            builder: _i34.previewSuccessSelectCertificateScreen,
           ),
         ],
       ),
@@ -438,14 +446,14 @@ final directories = <_i1.WidgetbookNode>[
         name: 'SettingsScreen',
         useCase: _i1.WidgetbookUseCase(
           name: 'SettingsScreen',
-          builder: _i34.previewSettingsScreen,
+          builder: _i35.previewSettingsScreen,
         ),
       ),
       _i1.WidgetbookLeafComponent(
         name: 'ShowDocumentScreen',
         useCase: _i1.WidgetbookUseCase(
           name: '',
-          builder: _i35.previewShowDocumentScreen,
+          builder: _i36.previewShowDocumentScreen,
         ),
       ),
       _i1.WidgetbookComponent(
@@ -453,15 +461,15 @@ final directories = <_i1.WidgetbookNode>[
         useCases: [
           _i1.WidgetbookUseCase(
             name: 'error',
-            builder: _i36.previewErrorSignDocumentScreen,
+            builder: _i37.previewErrorSignDocumentScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'loading',
-            builder: _i36.previewLoadingSignDocumentScreen,
+            builder: _i37.previewLoadingSignDocumentScreen,
           ),
           _i1.WidgetbookUseCase(
             name: 'success',
-            builder: _i36.previewSuccessSignDocumentScreen,
+            builder: _i37.previewSuccessSignDocumentScreen,
           ),
         ],
       ),
@@ -469,7 +477,7 @@ final directories = <_i1.WidgetbookNode>[
         name: 'StartRemoteDocumentSigningScreen',
         useCase: _i1.WidgetbookUseCase(
           name: '',
-          builder: _i37.previewStartRemoteDocumentSigningScreen,
+          builder: _i38.previewStartRemoteDocumentSigningScreen,
         ),
       ),
     ],
diff --git a/pubspec.lock b/pubspec.lock
index 1ef81f4..76991d1 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -417,6 +417,14 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_markdown:
+    dependency: "direct main"
+    description:
+      name: flutter_markdown
+      sha256: "5b24061317f850af858ef7151dadbb6eb77c1c449c954c7bb064e8a5e0e7d81f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.20"
   flutter_plugin_android_lifecycle:
     dependency: transitive
     description:
@@ -603,6 +611,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.2.0"
+  markdown:
+    dependency: transitive
+    description:
+      name: markdown
+      sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.2.2"
   matcher:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index fa2cca1..57adfa0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -46,6 +46,7 @@ dependencies:
   url_launcher:
   mobile_scanner:
   html:
+  flutter_markdown:
 
 dev_dependencies:
   flutter_test:

From 130b252adb87d0a1053fc0ae4580bbd88628d081 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marek=20Ce=C4=BEuch?= <celuchmarek@gmail.com>
Date: Sun, 30 Jun 2024 18:12:43 +0200
Subject: [PATCH 3/7] rm tokens from dependecny checkout and add .apk (#27)

---
 .github/workflows/android_package.yaml | 22 ++++++++++++++++++----
 1 file changed, 18 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/android_package.yaml b/.github/workflows/android_package.yaml
index ca0f212..6d9c927 100644
--- a/.github/workflows/android_package.yaml
+++ b/.github/workflows/android_package.yaml
@@ -31,14 +31,12 @@ jobs:
         uses: actions/checkout@v4
         with:
           repository: slovensko-digital/eidmsdk-flutter
-          token: ${{ secrets.GH_PAT }}
           path: eidmsdk_flutter
 
       - name: Checkout "autogram_sign"
         uses: actions/checkout@v4
         with:
           repository: slovensko-digital/avm-client-dart
-          token: ${{ secrets.GH_PAT }}
           path: autogram_sign
 
       - uses: actions/setup-java@v1
@@ -64,7 +62,7 @@ jobs:
         working-directory: ./app
         run: echo $ENCODED_STRING | base64 -d > release_keystore.jks
 
-      - name: Build
+      - name: Build Googla Play .aab
         env:
           AVM_KEYSTORE_FILE: ../../release_keystore.jks
           AVM_KEYSTORE_PASSWORD: ${{ secrets.GOOGLE_RELEASE_KEYSTORE_PASSWORD }}
@@ -73,12 +71,27 @@ jobs:
         working-directory: ./app
         run: flutter build appbundle --release
 
-      - name: Upload Release Build to Artifacts
+      - name: Build Andorid .apk
+        env:
+          AVM_KEYSTORE_FILE: ../../release_keystore.jks
+          AVM_KEYSTORE_PASSWORD: ${{ secrets.GOOGLE_RELEASE_KEYSTORE_PASSWORD }}
+          AVM_KEY_ALIAS: ${{ secrets.GOOGLE_RELEASE_KEYSTORE_ALIAS }}
+          AVM_KEY_PASSWORD: ${{ secrets.GOOGLE_RELEASE_KEY_PASSWORD }}
+        working-directory: ./app
+        run: flutter build apk --release
+
+      - name: Upload .aab Build to Artifacts
         uses: actions/upload-artifact@v3
         with:
            name: release-artifacts
            path: ./app/build/app/outputs/bundle/release/app-release.aab
 
+      - name: Upload .apk Build to Artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          name: release-artifacts
+          path: ./app/build/app/outputs/apk/release/app-release.apk
+
       - name: Create release if tag pushed
         uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844
         if: startsWith(github.ref, 'refs/tags/')
@@ -89,3 +102,4 @@ jobs:
           prerelease: true
           files: |
             ./app/build/app/outputs/bundle/release/app-release.aab
+            ./app/build/app/outputs/apk/release/app-release.apk

From 34fc6db9180c43505dd772102f2809d12de78a95 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marek=20Ce=C4=BEuch?= <celuchmarek@gmail.com>
Date: Tue, 2 Jul 2024 13:31:24 +0200
Subject: [PATCH 4/7] mv eidmsdk secret token from gradle build (#28)

* mv eidmsdk secret token from gradle build

* use github token to download eidmsdk pkg

* use personal pat for eidmsdk

* add info about gh pat env to readme
---
 .github/workflows/android_package.yaml | 2 ++
 README.md                              | 2 ++
 android/build.gradle                   | 2 +-
 3 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/android_package.yaml b/.github/workflows/android_package.yaml
index 6d9c927..b4da0b1 100644
--- a/.github/workflows/android_package.yaml
+++ b/.github/workflows/android_package.yaml
@@ -68,6 +68,7 @@ jobs:
           AVM_KEYSTORE_PASSWORD: ${{ secrets.GOOGLE_RELEASE_KEYSTORE_PASSWORD }}
           AVM_KEY_ALIAS: ${{ secrets.GOOGLE_RELEASE_KEYSTORE_ALIAS }}
           AVM_KEY_PASSWORD: ${{ secrets.GOOGLE_RELEASE_KEY_PASSWORD }}
+          EIDMSDK_ACCESS_TOKEN: ${{ secrets.GH_PAT }}
         working-directory: ./app
         run: flutter build appbundle --release
 
@@ -77,6 +78,7 @@ jobs:
           AVM_KEYSTORE_PASSWORD: ${{ secrets.GOOGLE_RELEASE_KEYSTORE_PASSWORD }}
           AVM_KEY_ALIAS: ${{ secrets.GOOGLE_RELEASE_KEYSTORE_ALIAS }}
           AVM_KEY_PASSWORD: ${{ secrets.GOOGLE_RELEASE_KEY_PASSWORD }}
+          EIDMSDK_ACCESS_TOKEN: ${{ secrets.GH_PAT }}
         working-directory: ./app
         run: flutter build apk --release
 
diff --git a/README.md b/README.md
index 1441f22..f981baa 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,8 @@ fvm dart run build_runner build --delete-conflicting-outputs
 
 Build **Android** APK:
 
+eid-mSDK binaries are hosted on GitHub package registry. To access the package during build process environment variable `EIDMSDK_ACCESS_TOKEN` needs to be set to a GitHub Personal Access Token that has permission to read package registry.
+
 ```shell
 fvm flutter build apk
 ```
diff --git a/android/build.gradle b/android/build.gradle
index 9298f83..d22d8a6 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -18,7 +18,7 @@ allprojects {
             url = "https://maven.pkg.github.com/eIDmSDK/eID-mSDK-Android/"
             credentials {
                 username = "eIDmSDK"
-                password = "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"
+                password = System.getenv("EIDMSDK_ACCESS_TOKEN")
             }
         }
     }

From b7838a28dc72b0ea55923f63afc7d55b4fcb114a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matej=20Hlatk=C3=BD?= <hlatky@freevision.sk>
Date: Mon, 15 Jul 2024 10:16:43 +0200
Subject: [PATCH 5/7] Crashlytics enhanced (#30)

---
 CHANGELOG.md                                  | 10 +++--
 lib/app_navigator_observer.dart               | 26 +++++++------
 lib/bloc/create_document_cubit.dart           |  2 +-
 lib/bloc/paired_device_list_cubit.dart        |  2 +-
 lib/bloc/present_signed_document_cubit.dart   | 11 +++---
 lib/bloc/preview_document_cubit.dart          |  2 +-
 lib/bloc/sign_document_cubit.dart             |  2 +-
 lib/deep_links.dart                           |  2 +-
 lib/file_extensions.dart                      |  7 ++++
 lib/main.dart                                 | 39 ++++++++++++-------
 .../select_signing_certificate_fragment.dart  |  1 +
 lib/ui/onboarding.dart                        |  7 +++-
 lib/ui/remote_document_signing.dart           |  7 +++-
 lib/ui/screens/main_menu_screen.dart          |  7 +++-
 lib/ui/screens/main_screen.dart               |  8 +++-
 .../present_signed_document_screen.dart       |  1 +
 lib/ui/screens/select_certificate_screen.dart |  1 +
 lib/ui/screens/settings_screen.dart           |  3 ++
 lib/utils.dart                                | 17 ++++++++
 pubspec.lock                                  | 16 ++++----
 pubspec.yaml                                  |  2 +-
 test/utils_test.dart                          | 36 +++++++++++++++++
 22 files changed, 155 insertions(+), 54 deletions(-)
 create mode 100644 test/utils_test.dart

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14c8076..cced286 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,15 @@
 # Changelog
 
-## NEXT - v1.0.2(33)
+## NEXT - v1.0.3(34)
+
+- #30 | Logging into Crashlytics
+
+## 2024-06-30 - v1.0.2(33)
  
 - Refactor Settings model
 - #18 | Allow sharing signed Document even when failed to save file
-- About screen - add links into "about" text
-- About screen - fix vertical content overflow on smaller screens
+- #26 | About screen - add links into "about" text
+- #26 | About screen - fix vertical content overflow on smaller screens
 
 ## 2024-06-17 - v1.0.1(1)
  
diff --git a/lib/app_navigator_observer.dart b/lib/app_navigator_observer.dart
index 5f66610..6c3257b 100644
--- a/lib/app_navigator_observer.dart
+++ b/lib/app_navigator_observer.dart
@@ -7,22 +7,22 @@ class AppNavigatorObserver extends NavigatorObserver {
 
   @override
   void didPush(Route route, Route? previousRoute) {
-    _log("Did push", {'route': route, 'previousRoute': previousRoute});
+    _log("didPush", {'route': route, 'previousRoute': previousRoute});
   }
 
   @override
   void didPop(Route route, Route? previousRoute) {
-    _log("Did pop", {'route': route, 'previousRoute': previousRoute});
+    _log("didPop", {'route': route, 'previousRoute': previousRoute});
   }
 
   @override
   void didRemove(Route route, Route? previousRoute) {
-    _log("Did remove", {'route': route, 'previousRoute': previousRoute});
+    _log("didRemove", {'route': route, 'previousRoute': previousRoute});
   }
 
   @override
   void didReplace({Route? newRoute, Route? oldRoute}) {
-    _log("Did replace", {'newRoute': newRoute, 'oldRoute': oldRoute});
+    _log("didReplace", {'newRoute': newRoute, 'oldRoute': oldRoute});
   }
 
   /// Logs navigation event.
@@ -32,22 +32,24 @@ class AppNavigatorObserver extends NavigatorObserver {
         .map((param) => "$param: ${routes[param]?.debug}")
         .join(", ");
 
-    _logger.info("$event - $params");
+    _logger.info("$event($params)");
   }
 }
 
 extension _RouteExtensions<T> on Route<T> {
   /// Returns debug string for this [Route].
   String? get debug {
-    final route = this;
+    final name = settings.name;
 
-    if (route is! MaterialPageRoute) {
-      return route.settings.name;
-    }
+    if (name != null && name.isNotEmpty) {
+      return name;
+    } else if (this is MaterialPageRoute) {
+      final text = (this as MaterialPageRoute?)?.builder.runtimeType.toString();
 
-    final text = (route as MaterialPageRoute).builder.runtimeType.toString();
+      // "(BuildContext) => NameScreen"
+      return text?.replaceFirst("(BuildContext) => ", "");
+    }
 
-    // "(BuildContext) => NameScreen"
-    return text.replaceFirst("(BuildContext) => ", "");
+    return null;
   }
 }
diff --git a/lib/bloc/create_document_cubit.dart b/lib/bloc/create_document_cubit.dart
index 32860d0..bdd48c9 100644
--- a/lib/bloc/create_document_cubit.dart
+++ b/lib/bloc/create_document_cubit.dart
@@ -22,7 +22,7 @@ export 'create_document_state.dart';
 ///  - [PreviewDocumentCubit]
 @injectable
 class CreateDocumentCubit extends Cubit<CreateDocumentState> {
-  static final _log = Logger("CreateDocumentCubit");
+  static final _log = Logger((CreateDocumentCubit).toString());
 
   final IAutogramService _service;
   final FutureOr<File> _file;
diff --git a/lib/bloc/paired_device_list_cubit.dart b/lib/bloc/paired_device_list_cubit.dart
index 99c9048..8970829 100644
--- a/lib/bloc/paired_device_list_cubit.dart
+++ b/lib/bloc/paired_device_list_cubit.dart
@@ -13,7 +13,7 @@ export 'paired_device_list_state.dart';
 /// Cubit for the [PairedDeviceListScreen] with only [load] function.
 @injectable
 class PairedDeviceListCubit extends Cubit<PairedDeviceListState> {
-  static final _log = Logger("PairedDeviceListScreen");
+  static final _log = Logger((PairedDeviceListScreen).toString());
 
   // ignore: unused_field
   final IAutogramService _service;
diff --git a/lib/bloc/present_signed_document_cubit.dart b/lib/bloc/present_signed_document_cubit.dart
index 550821c..3308d6e 100644
--- a/lib/bloc/present_signed_document_cubit.dart
+++ b/lib/bloc/present_signed_document_cubit.dart
@@ -23,7 +23,7 @@ export 'present_signed_document_state.dart';
 /// which can be shared.
 @injectable
 class PresentSignedDocumentCubit extends Cubit<PresentSignedDocumentState> {
-  static final _log = Logger("PresentSignedDocumentCubit");
+  static final _log = Logger((PresentSignedDocumentCubit).toString());
   static final _tsDateFormat = DateFormat('yyyyMMddHHmmss');
 
   final AppService _appService;
@@ -38,7 +38,8 @@ class PresentSignedDocumentCubit extends Cubit<PresentSignedDocumentState> {
 
   /// Saves [signedDocument] into public directory.
   Future<void> saveDocument() async {
-    _log.info("Saving signed document: ${signedDocument.filename}.");
+    _log.info(
+        "Saving signed document: ${File(signedDocument.filename).redactedInfo}.");
 
     emit(state.toLoading());
 
@@ -50,12 +51,12 @@ class PresentSignedDocumentCubit extends Cubit<PresentSignedDocumentState> {
       // Need to change PresentSignedDocumentSuccessState impl. to allow File?
       await _saveDocumentIntoFile(file!);
 
-      _log.info("Signed Document was saved into $file");
+      _log.info("Signed Document was saved into ${file.redactedInfo}");
 
       emit(state.toSuccess(file));
     } catch (error, stackTrace) {
-      _log.severe(
-          "Error saving signed Document into $file.", error, stackTrace);
+      _log.severe("Error saving signed Document into ${file?.redactedInfo}.",
+          error, stackTrace);
 
       emit(state.toError(error));
     }
diff --git a/lib/bloc/preview_document_cubit.dart b/lib/bloc/preview_document_cubit.dart
index 2650854..fe01598 100644
--- a/lib/bloc/preview_document_cubit.dart
+++ b/lib/bloc/preview_document_cubit.dart
@@ -15,7 +15,7 @@ export 'preview_document_state.dart';
 ///  - [CreateDocumentCubit]
 @injectable
 class PreviewDocumentCubit extends Cubit<PreviewDocumentState> {
-  static final _log = Logger("PreviewDocumentCubit");
+  static final _log = Logger((PreviewDocumentCubit).toString());
 
   final IAutogramService _service;
   final String documentId;
diff --git a/lib/bloc/sign_document_cubit.dart b/lib/bloc/sign_document_cubit.dart
index c000073..a3c2ccc 100644
--- a/lib/bloc/sign_document_cubit.dart
+++ b/lib/bloc/sign_document_cubit.dart
@@ -14,7 +14,7 @@ export 'sign_document_state.dart';
 /// Cubit for the [SignDocumentScreen].
 @injectable
 class SignDocumentCubit extends Cubit<SignDocumentState> {
-  static final _log = Logger("SignDocumentCubit");
+  static final _log = Logger((SignDocumentCubit).toString());
   static const _defaultLanguage = 'sk';
 
   final IAutogramService _service;
diff --git a/lib/deep_links.dart b/lib/deep_links.dart
index 94c2180..8807c6d 100644
--- a/lib/deep_links.dart
+++ b/lib/deep_links.dart
@@ -6,7 +6,7 @@
 ///  - path: "/api/v1/"
 ///
 /// Throws [ArgumentError] in case of invalid or unknown schema or structure.
-// TODO Move this code + test into autogram_sign module
+// TODO Move this code + test into autogram_sign module; however, it needs to check uri.authority, so it should be function on IAutogramService
 DeepLinkAction parseDeepLinkAction(Uri uri) {
   // Validate schema, authority and path
   switch (uri.scheme) {
diff --git a/lib/file_extensions.dart b/lib/file_extensions.dart
index 5bafb0c..2d84f8d 100644
--- a/lib/file_extensions.dart
+++ b/lib/file_extensions.dart
@@ -1,5 +1,6 @@
 import 'dart:io' show File;
 
+import 'package:flutter/foundation.dart';
 import 'package:path/path.dart' as path;
 
 /// Set of extensions on [File] type.
@@ -13,4 +14,10 @@ extension FileExtensions on File {
   /// Calls [path.basenameWithoutExtension] on this [File].
   String get basenameWithoutExtension =>
       path.basenameWithoutExtension(this.path);
+
+  /// Returns redacted file info usable for logging.
+  ///
+  /// In case of [kDebugMode], full [File.path] is returned;
+  /// "???.[FileExtensions.extension]" otherwise.
+  String get redactedInfo => (kDebugMode ? this.path : "???$extension");
 }
diff --git a/lib/main.dart b/lib/main.dart
index f2c70e6..47ea1d5 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,6 +1,8 @@
 import 'dart:developer' as developer;
 
 import 'package:firebase_core/firebase_core.dart';
+import 'package:firebase_crashlytics/firebase_crashlytics.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart' show WidgetsFlutterBinding, runApp;
 import 'package:logging/logging.dart' show Level, LogRecord, Logger;
 import 'package:provider/provider.dart' show MultiProvider, Provider;
@@ -9,20 +11,21 @@ import 'app.dart';
 import 'data/settings.dart';
 import 'di.dart';
 import 'firebase_options.dart';
+import 'utils.dart';
 
 void main() async {
   WidgetsFlutterBinding.ensureInitialized();
 
-  // Setup Logger
-  Logger.root
-    ..level = Level.ALL
-    ..onRecord.listen(_onRecord);
-
   // Setup Firebase
   await Firebase.initializeApp(
     options: DefaultFirebaseOptions.currentPlatform,
   );
 
+  // Setup Logger
+  Logger.root
+    ..level = Level.ALL
+    ..onRecord.listen(_onRecord);
+
   // Setup DI
   configureDependencies();
 
@@ -40,15 +43,21 @@ void main() async {
   );
 }
 
+/// [Logger] log callback.
 void _onRecord(final LogRecord record) {
-  developer.log(
-    record.message,
-    name: record.loggerName,
-    time: record.time,
-    sequenceNumber: record.sequenceNumber,
-    level: record.level.value,
-    zone: record.zone,
-    error: record.error,
-    stackTrace: record.stackTrace,
-  );
+  if (kDebugMode) {
+    developer.log(
+      record.message,
+      name: record.loggerName,
+      time: record.time,
+      sequenceNumber: record.sequenceNumber,
+      level: record.level.value,
+      zone: record.zone,
+      error: record.error,
+      stackTrace: record.stackTrace,
+    );
+  } else {
+    // TODO Collect also some Settings (acceptedPrivacyPolicyVersion, acceptedTermsOfServiceVersion) on app init and on change
+    FirebaseCrashlytics.instance.log(formatCrashlyticsLog(record));
+  }
 }
diff --git a/lib/ui/fragment/select_signing_certificate_fragment.dart b/lib/ui/fragment/select_signing_certificate_fragment.dart
index 5997739..779e497 100644
--- a/lib/ui/fragment/select_signing_certificate_fragment.dart
+++ b/lib/ui/fragment/select_signing_certificate_fragment.dart
@@ -65,6 +65,7 @@ class SelectSigningCertificateFragment extends StatelessWidget {
         Uri.parse(strings.selectSigningCertificateNoCertificateGuideUrl);
 
     final textStyle = Theme.of(context).textTheme.bodyMedium;
+    // TODO Use MarkdownText instead of RichText
     final body = RichText(
       text: TextSpan(
         children: [
diff --git a/lib/ui/onboarding.dart b/lib/ui/onboarding.dart
index 0bba5a0..b745247 100644
--- a/lib/ui/onboarding.dart
+++ b/lib/ui/onboarding.dart
@@ -62,7 +62,12 @@ abstract class Onboarding {
     Widget screen, [
     bool replace = false,
   ]) {
-    final route = MaterialPageRoute(builder: (_) => screen);
+    final route = MaterialPageRoute(
+      settings: RouteSettings(
+        name: screen.runtimeType.toString(),
+      ),
+      builder: (_) => screen,
+    );
     final navigator = Navigator.of(context);
 
     return replace ? navigator.pushReplacement(route) : navigator.push(route);
diff --git a/lib/ui/remote_document_signing.dart b/lib/ui/remote_document_signing.dart
index 19dba95..4e39d41 100644
--- a/lib/ui/remote_document_signing.dart
+++ b/lib/ui/remote_document_signing.dart
@@ -44,8 +44,13 @@ abstract class RemoteDocumentSigning {
     Widget screen, [
     bool replace = false,
   ]) {
+    final route = MaterialPageRoute(
+      settings: RouteSettings(
+        name: screen.runtimeType.toString(),
+      ),
+      builder: (_) => screen,
+    );
     final navigator = Navigator.of(context);
-    final route = MaterialPageRoute(builder: (_) => screen);
 
     return replace ? navigator.pushReplacement(route) : navigator.push(route);
   }
diff --git a/lib/ui/screens/main_menu_screen.dart b/lib/ui/screens/main_menu_screen.dart
index 3e8e7b4..b49bb8b 100644
--- a/lib/ui/screens/main_menu_screen.dart
+++ b/lib/ui/screens/main_menu_screen.dart
@@ -127,7 +127,12 @@ class MainMenuScreen extends StatelessWidget {
   }
 
   static Future<void> _openScreen(BuildContext context, Widget screen) {
-    final route = MaterialPageRoute(builder: (_) => screen);
+    final route = MaterialPageRoute(
+      settings: RouteSettings(
+        name: screen.runtimeType.toString(),
+      ),
+      builder: (_) => screen,
+    );
 
     return Navigator.of(context).pushReplacement(route);
   }
diff --git a/lib/ui/screens/main_screen.dart b/lib/ui/screens/main_screen.dart
index 6542592..1c85709 100644
--- a/lib/ui/screens/main_screen.dart
+++ b/lib/ui/screens/main_screen.dart
@@ -14,6 +14,7 @@ import '../../app_service.dart';
 import '../../bloc/app_bloc.dart';
 import '../../deep_links.dart';
 import '../../di.dart';
+import '../../file_extensions.dart';
 import '../../services/encryption_key_registry.dart';
 import '../../strings_context.dart';
 import '../app_theme.dart';
@@ -126,6 +127,9 @@ class _MainScreenState extends State<MainScreen> {
 
     return showGeneralDialog(
       context: context,
+      routeSettings: RouteSettings(
+        name: screen.runtimeType.toString(),
+      ),
       pageBuilder: (context, _, __) => screen,
     );
   }
@@ -205,9 +209,9 @@ class _MainScreenState extends State<MainScreen> {
     final selectedFile = result?.files.singleOrNull;
 
     if (selectedFile != null) {
-      final File file = File(selectedFile.path!);
+      final file = File(selectedFile.path!);
 
-      _logger.fine('File selected: $file');
+      _logger.fine('File selected: ${file.redactedInfo}');
 
       if (context.mounted) {
         _openNewFile(file);
diff --git a/lib/ui/screens/present_signed_document_screen.dart b/lib/ui/screens/present_signed_document_screen.dart
index 777b8d9..983f78d 100644
--- a/lib/ui/screens/present_signed_document_screen.dart
+++ b/lib/ui/screens/present_signed_document_screen.dart
@@ -198,6 +198,7 @@ class _SuccessContent extends StatelessWidget {
         decoration: TextDecoration.underline,
         fontWeight: FontWeight.bold,
       );
+      // TODO Use MarkdownText instead of RichText
       body = RichText(
         text: TextSpan(
           text: strings.saveSignedDocumentSuccessMessage,
diff --git a/lib/ui/screens/select_certificate_screen.dart b/lib/ui/screens/select_certificate_screen.dart
index aefa393..74a78e3 100644
--- a/lib/ui/screens/select_certificate_screen.dart
+++ b/lib/ui/screens/select_certificate_screen.dart
@@ -232,6 +232,7 @@ class _SelectSignatureTypeContentState
                 error: state.error,
               ),
             GetDocumentSignatureTypeSuccessState state => SignatureTypePicker(
+                // TODO Check why not passing state.signatureType
                 value: _signatureType,
                 canChange: (widget.signingType == DocumentSigningType.local),
                 onValueChanged: (final SignatureType value) {
diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart
index 2cb3a96..a1922c8 100644
--- a/lib/ui/screens/settings_screen.dart
+++ b/lib/ui/screens/settings_screen.dart
@@ -205,6 +205,9 @@ class _ValueListenableBoundTile<T> extends StatelessWidget {
   Future<void> _onEditItemRequested(BuildContext context, T value) async {
     final result = await showDialog<T>(
       context: context,
+      routeSettings: RouteSettings(
+        name: "Edit${T}Dialog",
+      ),
       builder: (context) {
         var selectedValue = setting.value;
 
diff --git a/lib/utils.dart b/lib/utils.dart
index 445c059..b6060ac 100644
--- a/lib/utils.dart
+++ b/lib/utils.dart
@@ -3,6 +3,7 @@ import 'dart:math' show Random;
 
 import 'package:basic_utils/basic_utils.dart'
     show X509CertificateData, X509Utils;
+import 'package:logging/logging.dart' show LogRecord, Level;
 
 final Random _random = Random.secure();
 
@@ -26,3 +27,19 @@ X509CertificateData x509CertificateDataFromDer(String data) {
 
   return X509Utils.x509CertificateFromPem(pem);
 }
+
+/// Format given [log] as single message.
+String formatCrashlyticsLog(final LogRecord log) {
+  final level = switch (log.level) {
+    Level.FINEST || Level.FINER || Level.FINE || Level.CONFIG => 'D',
+    Level.INFO => 'I',
+    Level.WARNING => 'W',
+    Level.SEVERE || Level.SHOUT => 'E',
+    _ => "?"
+  };
+  final tag = log.loggerName;
+  final line1 = "${log.time}: $level/$tag: ${log.message}";
+  final error = log.error;
+
+  return (error != null ? "$line1\n$error" : line1);
+}
diff --git a/pubspec.lock b/pubspec.lock
index 76991d1..b672e47 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -455,10 +455,10 @@ packages:
     dependency: transitive
     description:
       name: freezed_annotation
-      sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
+      sha256: f9f6597ac43cc262fa7d7f2e65259a6060c23a560525d1f2631be374540f2a9b
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.1"
+    version: "2.4.3"
   frontend_server_client:
     dependency: transitive
     description:
@@ -775,10 +775,10 @@ packages:
     dependency: transitive
     description:
       name: path_provider_windows
-      sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
+      sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.1"
+    version: "2.3.0"
   pdf:
     dependency: "direct main"
     description:
@@ -1212,10 +1212,10 @@ packages:
     dependency: transitive
     description:
       name: uuid
-      sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
+      sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90"
       url: "https://pub.dev"
     source: hosted
-    version: "4.4.0"
+    version: "4.4.2"
   vector_graphics:
     dependency: transitive
     description:
@@ -1356,10 +1356,10 @@ packages:
     dependency: "direct main"
     description:
       name: widgetbook
-      sha256: "872e7e9065ef6e85a1e93b3b41830f90af575c5a898b6c573acdc972fad0fb29"
+      sha256: de5b9887f9ad663bdcc1f957bc22aa0eb4dd7b406ac0142158bf35c0fad4a4f2
       url: "https://pub.dev"
     source: hosted
-    version: "3.8.0"
+    version: "3.8.1"
   widgetbook_annotation:
     dependency: "direct main"
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 57adfa0..eb1a571 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.2+33
+version: 1.0.3+34
 
 environment:
   sdk: '>=3.2.3 <4.0.0'
diff --git a/test/utils_test.dart b/test/utils_test.dart
new file mode 100644
index 0000000..21def36
--- /dev/null
+++ b/test/utils_test.dart
@@ -0,0 +1,36 @@
+import 'dart:io';
+
+import 'package:autogram/utils.dart';
+import 'package:logging/logging.dart' show Level, LogRecord;
+import 'package:test/test.dart';
+
+/// Tests for the [formatCrashlyticsLog] function.
+void main() {
+  group('formatCrashlyticsLog', () {
+    test('formatCrashlyticsLog formats simple log properly', () {
+      final infoLog = LogRecord(Level.INFO, 'Hello world!', 'Logger1');
+      final configLog =
+          LogRecord(Level.CONFIG, 'This has been configured.', 'ConfigLogger');
+
+      expect(
+        formatCrashlyticsLog(infoLog),
+        '${infoLog.time}: I/Logger1: Hello world!',
+      );
+      expect(
+        formatCrashlyticsLog(configLog),
+        '${configLog.time}: D/ConfigLogger: This has been configured.',
+      );
+    });
+
+    test('formatCrashlyticsLog formats log with error properly', () {
+      const error = SocketException('No Internets!');
+      final errorLog =
+          LogRecord(Level.SEVERE, 'Unable to download file.', 'FileDownloader',  error);
+
+      expect(
+        formatCrashlyticsLog(errorLog),
+        '${errorLog.time}: E/FileDownloader: Unable to download file.\n$error',
+      );
+    });
+  });
+}

From acd9126a2e21f6c2d71aca43e7a77b272c02fcd8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matej=20Hlatk=C3=BD?= <hlatky@freevision.sk>
Date: Tue, 16 Jul 2024 13:40:33 +0200
Subject: [PATCH 6/7] Fix iOS Domain Association (#31)

---
 CHANGELOG.md                   |  3 +-
 ios/Podfile.lock               | 67 ++++++++++++++++++++++++++++++++++
 ios/Runner/Runner.entitlements |  4 ++
 3 files changed, 73 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cced286..df55f90 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,9 @@
 # Changelog
 
-## NEXT - v1.0.3(34)
+## 2024-07-16 - v1.0.3(34)
 
 - #30 | Logging into Crashlytics
+- #16 | Fix iOS URL domain association
 
 ## 2024-06-30 - v1.0.2(33)
  
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index fd6988d..be6f800 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -84,6 +84,23 @@ PODS:
     - GoogleUtilities/Environment (~> 7.7)
     - nanopb (< 2.30911.0, >= 2.30908.0)
     - PromisesObjC (< 3.0, >= 1.2)
+  - GoogleMLKit/BarcodeScanning (4.0.0):
+    - GoogleMLKit/MLKitCore
+    - MLKitBarcodeScanning (~> 3.0.0)
+  - GoogleMLKit/MLKitCore (4.0.0):
+    - MLKitCommon (~> 9.0.0)
+  - GoogleToolboxForMac/DebugUtils (2.3.2):
+    - GoogleToolboxForMac/Defines (= 2.3.2)
+  - GoogleToolboxForMac/Defines (2.3.2)
+  - GoogleToolboxForMac/Logger (2.3.2):
+    - GoogleToolboxForMac/Defines (= 2.3.2)
+  - "GoogleToolboxForMac/NSData+zlib (2.3.2)":
+    - GoogleToolboxForMac/Defines (= 2.3.2)
+  - "GoogleToolboxForMac/NSDictionary+URLArguments (2.3.2)":
+    - GoogleToolboxForMac/DebugUtils (= 2.3.2)
+    - GoogleToolboxForMac/Defines (= 2.3.2)
+    - "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)"
+  - "GoogleToolboxForMac/NSString+URLArguments (2.3.2)"
   - GoogleUtilities/Environment (7.13.0):
     - GoogleUtilities/Privacy
     - PromisesObjC (< 3.0, >= 1.2)
@@ -96,8 +113,32 @@ PODS:
   - GoogleUtilities/UserDefaults (7.13.0):
     - GoogleUtilities/Logger
     - GoogleUtilities/Privacy
+  - GoogleUtilitiesComponents (1.1.0):
+    - GoogleUtilities/Logger
+  - GTMSessionFetcher/Core (2.3.0)
   - JWTDecode (3.1.0)
   - lottie-ios (4.4.0)
+  - MLImage (1.0.0-beta4)
+  - MLKitBarcodeScanning (3.0.0):
+    - MLKitCommon (~> 9.0)
+    - MLKitVision (~> 5.0)
+  - MLKitCommon (9.0.0):
+    - GoogleDataTransport (~> 9.0)
+    - GoogleToolboxForMac/Logger (~> 2.1)
+    - "GoogleToolboxForMac/NSData+zlib (~> 2.1)"
+    - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)"
+    - GoogleUtilities/UserDefaults (~> 7.0)
+    - GoogleUtilitiesComponents (~> 1.0)
+    - GTMSessionFetcher/Core (< 3.0, >= 1.1)
+  - MLKitVision (5.0.0):
+    - GoogleToolboxForMac/Logger (~> 2.1)
+    - "GoogleToolboxForMac/NSData+zlib (~> 2.1)"
+    - GTMSessionFetcher/Core (< 3.0, >= 1.1)
+    - MLImage (= 1.0.0-beta4)
+    - MLKitCommon (~> 9.0)
+  - mobile_scanner (3.5.6):
+    - Flutter
+    - GoogleMLKit/BarcodeScanning (~> 4.0.0)
   - nanopb (2.30910.0):
     - nanopb/decode (= 2.30910.0)
     - nanopb/encode (= 2.30910.0)
@@ -125,6 +166,8 @@ PODS:
   - SwiftyGif (5.4.4)
   - url_launcher_ios (0.0.1):
     - Flutter
+  - wakelock_plus (0.0.1):
+    - Flutter
   - webview_flutter_wkwebview (0.0.1):
     - Flutter
 
@@ -134,12 +177,14 @@ DEPENDENCIES:
   - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
   - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
   - Flutter (from `Flutter`)
+  - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
   - printing (from `.symlinks/plugins/printing/ios`)
   - share_plus (from `.symlinks/plugins/share_plus/ios`)
   - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
   - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+  - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
   - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
 
 SPEC REPOS:
@@ -154,9 +199,17 @@ SPEC REPOS:
     - FirebaseInstallations
     - FirebaseSessions
     - GoogleDataTransport
+    - GoogleMLKit
+    - GoogleToolboxForMac
     - GoogleUtilities
+    - GoogleUtilitiesComponents
+    - GTMSessionFetcher
     - JWTDecode
     - lottie-ios
+    - MLImage
+    - MLKitBarcodeScanning
+    - MLKitCommon
+    - MLKitVision
     - nanopb
     - OpenSSL-Universal
     - PromisesObjC
@@ -175,6 +228,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/firebase_crashlytics/ios"
   Flutter:
     :path: Flutter
+  mobile_scanner:
+    :path: ".symlinks/plugins/mobile_scanner/ios"
   package_info_plus:
     :path: ".symlinks/plugins/package_info_plus/ios"
   path_provider_foundation:
@@ -187,6 +242,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
   url_launcher_ios:
     :path: ".symlinks/plugins/url_launcher_ios/ios"
+  wakelock_plus:
+    :path: ".symlinks/plugins/wakelock_plus/ios"
   webview_flutter_wkwebview:
     :path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
 
@@ -206,9 +263,18 @@ SPEC CHECKSUMS:
   FirebaseSessions: 2651b464e241c93fd44112f995d5ab663c970487
   Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
   GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
+  GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e
+  GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34
   GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152
+  GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
+  GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2
   JWTDecode: 3eaab1e06b6f4dcbdd6716aff09ba4c2104ca8b7
   lottie-ios: ef1be1f90d54255f08e09d767950e43714661178
+  MLImage: 7bb7c4264164ade9bf64f679b40fb29c8f33ee9b
+  MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505
+  MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390
+  MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49
+  mobile_scanner: 38dcd8a49d7d485f632b7de65e4900010187aef2
   nanopb: 438bc412db1928dac798aa6fd75726007be04262
   OpenSSL-Universal: 6e1ae0555546e604dbc632a2b9a24a9c46c41ef6
   package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
@@ -221,6 +287,7 @@ SPEC CHECKSUMS:
   shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
   SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
   url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
+  wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
   webview_flutter_wkwebview: 4f3e50f7273d31e5500066ed267e3ae4309c5ae4
 
 PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
index 2bb4dee..44bec6d 100644
--- a/ios/Runner/Runner.entitlements
+++ b/ios/Runner/Runner.entitlements
@@ -2,6 +2,10 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>com.apple.developer.associated-domains</key>
+	<array>
+		<string>applinks:autogram.slovensko.digital</string>
+	</array>
 	<key>com.apple.developer.nfc.readersession.formats</key>
 	<array>
 		<string>TAG</string>

From 83dbbefa08071d3e679af0b35a4b659cf2c6c8be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matej=20Hlatk=C3=BD?= <hlatky@freevision.sk>
Date: Wed, 31 Jul 2024 09:33:34 +0200
Subject: [PATCH 7/7] Cleanup (#32)

---
 lib/app_service.dart                          |  2 +-
 lib/bloc/create_document_cubit.dart           |  2 +-
 lib/bloc/present_signed_document_cubit.dart   |  1 +
 lib/directory_extensions.dart                 |  4 +-
 lib/file_extensions.dart                      | 15 +----
 lib/file_system_entity_extensions.dart        | 23 +++++++
 lib/files.dart                                |  2 +-
 lib/l10n/app_localizations.dart               | 12 +---
 lib/l10n/app_localizations_sk.dart            |  9 ++-
 lib/l10n/app_sk.arb                           |  5 +-
 .../select_signing_certificate_fragment.dart  | 38 ++----------
 .../present_signed_document_screen.dart       | 61 +++++++++----------
 lib/ui/screens/preview_document_screen.dart   |  2 +-
 pubspec.lock                                  | 20 +++---
 14 files changed, 85 insertions(+), 111 deletions(-)
 create mode 100644 lib/file_system_entity_extensions.dart

diff --git a/lib/app_service.dart b/lib/app_service.dart
index f863b1e..3ccde09 100644
--- a/lib/app_service.dart
+++ b/lib/app_service.dart
@@ -6,7 +6,7 @@ import 'package:injectable/injectable.dart';
 import 'package:logging/logging.dart' show Logger;
 import 'package:path_provider/path_provider.dart' as provider;
 
-import 'file_extensions.dart';
+import 'file_system_entity_extensions.dart';
 
 /// Provides platform (iOS and Android) specific app functions:
 ///
diff --git a/lib/bloc/create_document_cubit.dart b/lib/bloc/create_document_cubit.dart
index bdd48c9..52c8c37 100644
--- a/lib/bloc/create_document_cubit.dart
+++ b/lib/bloc/create_document_cubit.dart
@@ -8,7 +8,7 @@ import 'package:injectable/injectable.dart';
 import 'package:logging/logging.dart';
 
 import '../data/pdf_signing_option.dart';
-import '../file_extensions.dart';
+import '../file_system_entity_extensions.dart';
 import '../files.dart';
 import '../ui/screens/open_document_screen.dart';
 import 'create_document_state.dart';
diff --git a/lib/bloc/present_signed_document_cubit.dart b/lib/bloc/present_signed_document_cubit.dart
index 3308d6e..9de8cfb 100644
--- a/lib/bloc/present_signed_document_cubit.dart
+++ b/lib/bloc/present_signed_document_cubit.dart
@@ -12,6 +12,7 @@ import 'package:path_provider/path_provider.dart';
 
 import '../app_service.dart';
 import '../file_extensions.dart';
+import '../file_system_entity_extensions.dart';
 import '../ui/screens/present_signed_document_screen.dart';
 import 'present_signed_document_state.dart';
 
diff --git a/lib/directory_extensions.dart b/lib/directory_extensions.dart
index 8e18dd8..a412832 100644
--- a/lib/directory_extensions.dart
+++ b/lib/directory_extensions.dart
@@ -1,12 +1,12 @@
 import 'dart:io' show Directory, File;
 
-import 'package:path/path.dart' as p;
+import 'package:path/path.dart' as path;
 
 /// Set of extensions on [Directory] type.
 extension DirectoryExtensions on Directory {
   /// Returns flag indicating whether can write into this [Directory].
   Future<bool> canWrite() async {
-    final tempFile = File(p.join(path, ".can_write"));
+    final tempFile = File(path.join(this.path, ".can_write"));
 
     try {
       await tempFile.writeAsBytes(const [], flush: true);
diff --git a/lib/file_extensions.dart b/lib/file_extensions.dart
index 2d84f8d..edff7df 100644
--- a/lib/file_extensions.dart
+++ b/lib/file_extensions.dart
@@ -1,23 +1,14 @@
 import 'dart:io' show File;
 
 import 'package:flutter/foundation.dart';
-import 'package:path/path.dart' as path;
+
+import 'file_system_entity_extensions.dart';
 
 /// Set of extensions on [File] type.
 extension FileExtensions on File {
-  /// Calls [path.extension] on this [File].
-  String get extension => path.extension(this.path);
-
-  /// Calls [path.basename] on this [File].
-  String get basename => path.basename(this.path);
-
-  /// Calls [path.basenameWithoutExtension] on this [File].
-  String get basenameWithoutExtension =>
-      path.basenameWithoutExtension(this.path);
-
   /// Returns redacted file info usable for logging.
   ///
   /// In case of [kDebugMode], full [File.path] is returned;
   /// "???.[FileExtensions.extension]" otherwise.
-  String get redactedInfo => (kDebugMode ? this.path : "???$extension");
+  String get redactedInfo => (kDebugMode ? path : "???$extension");
 }
diff --git a/lib/file_system_entity_extensions.dart b/lib/file_system_entity_extensions.dart
new file mode 100644
index 0000000..268c31a
--- /dev/null
+++ b/lib/file_system_entity_extensions.dart
@@ -0,0 +1,23 @@
+import 'dart:io' show FileSystemEntity;
+
+import 'package:path/path.dart' as path;
+
+import 'directory_extensions.dart';
+import 'file_extensions.dart';
+
+/// Set of extensions on [FileSystemEntity] type.
+///
+/// See also:
+///  - [FileExtensions]
+///  - [DirectoryExtensions]
+extension FileSystemEntityExtensions on FileSystemEntity {
+  /// Calls [path.extension] on this [FileSystemEntity.path].
+  String get extension => path.extension(this.path);
+
+  /// Calls [path.basename] on this [FileSystemEntity.path].
+  String get basename => path.basename(this.path);
+
+  /// Calls [path.basenameWithoutExtension] on this [FileSystemEntity.path].
+  String get basenameWithoutExtension =>
+      path.basenameWithoutExtension(this.path);
+}
diff --git a/lib/files.dart b/lib/files.dart
index dd8418b..a87cebe 100644
--- a/lib/files.dart
+++ b/lib/files.dart
@@ -1,6 +1,6 @@
 import 'dart:io' show File;
 
-import 'file_extensions.dart';
+import 'file_system_entity_extensions.dart';
 
 abstract class Files {
   /// Mapping of extension => MIME type.
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index b6f8a2f..58b4080 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -490,15 +490,9 @@ abstract class AppLocalizations {
   /// No description provided for @selectSigningCertificateNoCertificateBody.
   ///
   /// In sk, this message translates to:
-  /// **'Nepodarilo sa nájsť certifikát pre kvalifikovaný elektronický podpis.\n\nCertifikát je potrebné vydať v aplikácii eID klient, prípadne použiť iný občiansky preukaz.\nNávod na vydanie certifikátu nájdete na '**
+  /// **'Nepodarilo sa nájsť certifikát pre **kvalifikovaný elektronický podpis**.\n\nCertifikát je potrebné vydať v aplikácii “eID Klient”, prípadne použiť iný občiansky preukaz.\nNávod na vydanie certifikátu nájdete na [navody.digital](https://navody.digital/zivotne-situacie/aktivacia-eid/krok/certifikaty).'**
   String get selectSigningCertificateNoCertificateBody;
 
-  /// No description provided for @selectSigningCertificateNoCertificateGuideUrl.
-  ///
-  /// In sk, this message translates to:
-  /// **'https://navody.digital/zivotne-situacie/aktivacia-eid/krok/certifikaty'**
-  String get selectSigningCertificateNoCertificateGuideUrl;
-
   /// No description provided for @selectSigningCertificateErrorHeading.
   ///
   /// In sk, this message translates to:
@@ -544,8 +538,8 @@ abstract class AppLocalizations {
   /// No description provided for @saveSignedDocumentSuccessMessage.
   ///
   /// In sk, this message translates to:
-  /// **'Dokument bol uložený do Downloads pod názvom '**
-  String get saveSignedDocumentSuccessMessage;
+  /// **'Dokument bol uložený do **{directory}** pod názvom [{name}](#).'**
+  String saveSignedDocumentSuccessMessage(Object directory, Object name);
 
   /// No description provided for @saveSignedDocumentErrorMessage.
   ///
diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart
index 43ff162..c869bf9 100644
--- a/lib/l10n/app_localizations_sk.dart
+++ b/lib/l10n/app_localizations_sk.dart
@@ -253,10 +253,7 @@ class AppLocalizationsSk extends AppLocalizations {
   String get selectSigningCertificateNoCertificateHeading => 'Certifikát nebol nájdený';
 
   @override
-  String get selectSigningCertificateNoCertificateBody => 'Nepodarilo sa nájsť certifikát pre kvalifikovaný elektronický podpis.\n\nCertifikát je potrebné vydať v aplikácii eID klient, prípadne použiť iný občiansky preukaz.\nNávod na vydanie certifikátu nájdete na ';
-
-  @override
-  String get selectSigningCertificateNoCertificateGuideUrl => 'https://navody.digital/zivotne-situacie/aktivacia-eid/krok/certifikaty';
+  String get selectSigningCertificateNoCertificateBody => 'Nepodarilo sa nájsť certifikát pre **kvalifikovaný elektronický podpis**.\n\nCertifikát je potrebné vydať v aplikácii “eID Klient”, prípadne použiť iný občiansky preukaz.\nNávod na vydanie certifikátu nájdete na [navody.digital](https://navody.digital/zivotne-situacie/aktivacia-eid/krok/certifikaty).';
 
   @override
   String get selectSigningCertificateErrorHeading => 'Chyba pri načítavaní certifikátov z občianskeho preukazu.';
@@ -284,7 +281,9 @@ class AppLocalizationsSk extends AppLocalizations {
   String get documentSigningSuccessTitle => 'Dokument bol úspešne podpísaný';
 
   @override
-  String get saveSignedDocumentSuccessMessage => 'Dokument bol uložený do Downloads pod názvom ';
+  String saveSignedDocumentSuccessMessage(Object directory, Object name) {
+    return 'Dokument bol uložený do **$directory** pod názvom [$name](#).';
+  }
 
   @override
   String saveSignedDocumentErrorMessage(Object error) {
diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb
index 17b2485..5ddffae 100644
--- a/lib/l10n/app_sk.arb
+++ b/lib/l10n/app_sk.arb
@@ -79,8 +79,7 @@
   "selectSigningCertificateCanceledHeading": "Čítanie certifikátu bolo prerušené",
   "selectSigningCertificateCanceledBody": "Skúste prosím znovu načítať certifikát z vášho občianskeho preukazu.",
   "selectSigningCertificateNoCertificateHeading": "Certifikát nebol nájdený",
-  "selectSigningCertificateNoCertificateBody": "Nepodarilo sa nájsť certifikát pre kvalifikovaný elektronický podpis.\n\nCertifikát je potrebné vydať v aplikácii eID klient, prípadne použiť iný občiansky preukaz.\nNávod na vydanie certifikátu nájdete na ",
-  "selectSigningCertificateNoCertificateGuideUrl": "https://navody.digital/zivotne-situacie/aktivacia-eid/krok/certifikaty",
+  "selectSigningCertificateNoCertificateBody": "Nepodarilo sa nájsť certifikát pre **kvalifikovaný elektronický podpis**.\n\nCertifikát je potrebné vydať v aplikácii “eID Klient”, prípadne použiť iný občiansky preukaz.\nNávod na vydanie certifikátu nájdete na [navody.digital](https://navody.digital/zivotne-situacie/aktivacia-eid/krok/certifikaty).",
 
   "selectSigningCertificateErrorHeading": "Chyba pri načítavaní certifikátov z občianskeho preukazu.",
 
@@ -92,7 +91,7 @@
   "signDocumentErrorHeading": "Pri podpisovaní sa vyskytla chyba",
 
   "documentSigningSuccessTitle": "Dokument bol úspešne podpísaný",
-  "saveSignedDocumentSuccessMessage": "Dokument bol uložený do Downloads pod názvom ",
+  "saveSignedDocumentSuccessMessage": "Dokument bol uložený do **{directory}** pod názvom [{name}](#).",
   "saveSignedDocumentErrorMessage": "Pri ukladaní súboru sa vyskytla chyba:\n{error}",
 
   "shareSignedDocumentLabel": "Zdieľať podpísaný dokument",
diff --git a/lib/ui/fragment/select_signing_certificate_fragment.dart b/lib/ui/fragment/select_signing_certificate_fragment.dart
index 779e497..fb99bdb 100644
--- a/lib/ui/fragment/select_signing_certificate_fragment.dart
+++ b/lib/ui/fragment/select_signing_certificate_fragment.dart
@@ -1,12 +1,11 @@
 import 'package:eidmsdk/types.dart' show Certificate;
-import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
-import 'package:url_launcher/url_launcher.dart' show launchUrl;
 
 import '../../bloc/select_signing_certificate_cubit.dart';
 import '../../strings_context.dart';
 import '../widgets/error_content.dart';
 import '../widgets/loading_content.dart';
+import '../widgets/markdown_text.dart';
 import '../widgets/result_view.dart';
 
 /// Fragment that is used in screens working with [SelectSigningCertificateState].
@@ -61,42 +60,13 @@ class SelectSigningCertificateFragment extends StatelessWidget {
   /// Partial content for "no certificates" state.
   static Widget noCertificatesContent(BuildContext context) {
     final strings = context.strings;
-    final address =
-        Uri.parse(strings.selectSigningCertificateNoCertificateGuideUrl);
-
-    final textStyle = Theme.of(context).textTheme.bodyMedium;
-    // TODO Use MarkdownText instead of RichText
-    final body = RichText(
-      text: TextSpan(
-        children: [
-          TextSpan(
-            text: strings.selectSigningCertificateNoCertificateBody,
-            style: textStyle,
-          ),
-          TextSpan(
-            text: address.authority,
-            style: textStyle?.copyWith(
-              color: Theme.of(context).colorScheme.primary,
-              decoration: TextDecoration.underline,
-              fontWeight: FontWeight.bold,
-            ),
-            recognizer: TapGestureRecognizer()
-              ..onTap = () {
-                launchUrl(address);
-              },
-          ),
-          TextSpan(
-            text: ".",
-            style: textStyle,
-          ),
-        ],
-      ),
-    );
 
     return ResultView(
       icon: 'assets/images/lock.svg',
       titleText: strings.selectSigningCertificateNoCertificateHeading,
-      body: body,
+      body: MarkdownText(
+        strings.selectSigningCertificateNoCertificateBody,
+      ),
     );
   }
 }
diff --git a/lib/ui/screens/present_signed_document_screen.dart b/lib/ui/screens/present_signed_document_screen.dart
index 983f78d..23bad56 100644
--- a/lib/ui/screens/present_signed_document_screen.dart
+++ b/lib/ui/screens/present_signed_document_screen.dart
@@ -2,7 +2,7 @@ import 'dart:developer' as developer;
 import 'dart:io' show File, OSError, PathAccessException;
 
 import 'package:autogram_sign/autogram_sign.dart' show SignDocumentResponseBody;
-import 'package:flutter/gestures.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:share_plus/share_plus.dart';
@@ -12,11 +12,12 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
 import '../../bloc/present_signed_document_cubit.dart';
 import '../../data/document_signing_type.dart';
 import '../../di.dart';
-import '../../file_extensions.dart';
+import '../../file_system_entity_extensions.dart';
 import '../../strings_context.dart';
 import '../../util/errors.dart';
 import '../app_theme.dart';
 import '../widgets/loading_content.dart';
+import '../widgets/markdown_text.dart';
 import '../widgets/result_view.dart';
 
 /// Screen for presenting signed document.
@@ -153,11 +154,7 @@ class _Body extends StatelessWidget {
         sharingEnabled ? this.onShareFileRequested : null;
 
     return switch (state) {
-      PresentSignedDocumentInitialState _ => _SuccessContent(
-          file: null,
-          onShareFileRequested: null,
-          onCloseRequested: onCloseRequested,
-        ),
+      PresentSignedDocumentInitialState _ => const LoadingContent(),
       PresentSignedDocumentLoadingState _ => const LoadingContent(),
       PresentSignedDocumentErrorState _ => _SuccessContent(
           file: null,
@@ -193,27 +190,15 @@ class _SuccessContent extends StatelessWidget {
     Widget body = const SizedBox(height: 58);
 
     if (file != null) {
-      final fileNameTextStyle = TextStyle(
-        color: Theme.of(context).colorScheme.primary,
-        decoration: TextDecoration.underline,
-        fontWeight: FontWeight.bold,
-      );
-      // TODO Use MarkdownText instead of RichText
-      body = RichText(
-        text: TextSpan(
-          text: strings.saveSignedDocumentSuccessMessage,
-          style: Theme.of(context).textTheme.bodyLarge,
-          //style: TextStyle(color: Theme.of(context).colorScheme.onBackground),
-          children: [
-            // Emphasize file name
-            TextSpan(
-              text: file.basename,
-              style: fileNameTextStyle,
-              recognizer: TapGestureRecognizer()..onTap = onShareFileRequested,
-            )
-          ],
-        ),
-        textAlign: TextAlign.center,
+      final directory = _getParentDirectoryName(file);
+      final name = file.basename;
+      final text = strings.saveSignedDocumentSuccessMessage(directory, name);
+
+      body = MarkdownText(
+        text,
+        onLinkTap: (_, __, ___) {
+          onShareFileRequested?.call();
+        },
       );
     }
 
@@ -249,6 +234,18 @@ class _SuccessContent extends StatelessWidget {
       ],
     );
   }
+
+  static String _getParentDirectoryName(File file) {
+    return kIsWeb
+        ? file.uri
+                .resolve('.')
+                .path
+                .split('/')
+                .where((e) => e.isNotEmpty)
+                .lastOrNull ??
+            '&nbsp;'
+        : file.parent.basename;
+  }
 }
 
 @widgetbook.UseCase(
@@ -341,11 +338,11 @@ Widget previewSuccessPresentSignedDocumentScreen(BuildContext context) {
     options: DocumentSigningType.values,
     initialOption: DocumentSigningType.local,
   );
-  final fileName = context.knobs.string(
-    label: "File name",
-    initialValue: "document_signed.pdf",
+  final path = context.knobs.string(
+    label: "File path",
+    initialValue: "Downloads/document_signed.pdf",
   );
-  final file = File(fileName);
+  final file = File(path);
 
   return _Body(
     state: PresentSignedDocumentSuccessState(file),
diff --git a/lib/ui/screens/preview_document_screen.dart b/lib/ui/screens/preview_document_screen.dart
index ed08e0c..df411fc 100644
--- a/lib/ui/screens/preview_document_screen.dart
+++ b/lib/ui/screens/preview_document_screen.dart
@@ -11,7 +11,7 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
 
 import '../../bloc/preview_document_cubit.dart';
 import '../../data/document_signing_type.dart';
-import '../../file_extensions.dart';
+import '../../file_system_entity_extensions.dart';
 import '../../strings_context.dart';
 import '../app_theme.dart';
 import '../widgets/document_visualization.dart';
diff --git a/pubspec.lock b/pubspec.lock
index b672e47..b66192c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -347,10 +347,10 @@ packages:
     dependency: transitive
     description:
       name: firebase_core_platform_interface
-      sha256: "1003a5a03a61fc9a22ef49f37cbcb9e46c86313a7b2e7029b9390cf8c6fc32cb"
+      sha256: "3c3a1e92d6f4916c32deea79c4a7587aa0e9dbbe5889c7a16afcf005a485ee02"
       url: "https://pub.dev"
     source: hosted
-    version: "5.1.0"
+    version: "5.2.0"
   firebase_core_web:
     dependency: transitive
     description:
@@ -455,10 +455,10 @@ packages:
     dependency: transitive
     description:
       name: freezed_annotation
-      sha256: f9f6597ac43cc262fa7d7f2e65259a6060c23a560525d1f2631be374540f2a9b
+      sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.3"
+    version: "2.4.4"
   frontend_server_client:
     dependency: transitive
     description:
@@ -879,10 +879,10 @@ packages:
     dependency: transitive
     description:
       name: qs_dart
-      sha256: "5f1827ccdfa061582c121e7a8fe4a83319fa455bcd1fd6e46ff5b17b57aed680"
+      sha256: bc7ec5dab9b4d92b5404146736f99953d38a68ffe65cafe0ebb6952e20261571
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.0"
+    version: "1.2.1"
   recase:
     dependency: transitive
     description:
@@ -967,10 +967,10 @@ packages:
     dependency: transitive
     description:
       name: shared_preferences_platform_interface
-      sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
+      sha256: "034650b71e73629ca08a0bd789fd1d83cc63c2d1e405946f7cef7bc37432f93a"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "2.4.0"
   shared_preferences_web:
     dependency: transitive
     description:
@@ -1204,10 +1204,10 @@ packages:
     dependency: transitive
     description:
       name: url_launcher_windows
-      sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
+      sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.1"
+    version: "3.1.2"
   uuid:
     dependency: transitive
     description: